feat: add /api/v2/aibridge/serve endpoint - #26506
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
aaa5bce to
00065d0
Compare
441ef79 to
2566351
Compare
00065d0 to
3a82a8f
Compare
2566351 to
6ba893e
Compare
Docs preview📖 View docs preview for |
|
/coder-agents-review |
|
Chat: Review posted | View chat Review historydeep-review v0.9.0 | Round 2 | Last posted: Round 2, 18 findings (1 P1, 2 P2, 10 P3, 1 P4, 4 Nit), REQUEST_CHANGES. Review Finding inventoryFindings
Contested and acknowledgedCRF-3 (P1, enterprise/coderd/aibridgeserve.go:160) - Delegated auth path exposed over network boundary
Round logRound 1Panel. CRF-1 through CRF-13 posted. 1 P1, 1 P2, 5 P3, 1 P4, 3 Nit, 4 dropped Notes. Reviewed against 3a82a8f..3ca7c1b. Panel: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Ging-Go, Gon, Leorio, Ryosuke, Kurapika, Takumi, Chopper, Killua, Kite, Knov, Meruem, Komugi, Robin, Zoro, Razor, Melody, Knuckle. Netero pre-panel. Round 2Panel. 12 of 13 R1 findings addressed. CRF-3 contested by author, re-raised by panel. 5 new findings (CRF-18 through CRF-22). 1 P2, 4 P3. Reviewed against 65dff84..d1181b3. Panel: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Kurapika, Chopper, Komugi, Razor, Knov. Netero pre-panel. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
The transport architecture is clean: WebSocket, yamux, DRPC mirroring provisionerDaemonServe, with the shared Register extraction eliminating service-set drift. The test suite covers the auth boundary well (5 negative cases + full DRPC round trip). The YamuxDefaultStreamWindowSize constant is a good deduplication.
Severity count: 1 P1, 1 P2, 5 P3, 1 P4, 3 Nit.
The P1 is a security boundary violation. The IsAuthorized DRPC method has a delegated auth path (KeyId-only requests skip secret validation) that was designed exclusively for the in-process MemTransportPipe. The SECURITY comment on that method explicitly says: "Do not bind this DRPCServer to a network listener." This PR registers the same Server on a network-accessible mux via aibridgedserver.Register, exposing the delegated path to any gateway key holder.
The P2 (key revocation not terminating active sessions) is amplified by the P1: a revoked key's session persists with the ability to impersonate users. Five reviewers converged on this independently.
Process note: the commit scope enterprise/coderd is too narrow for a 14-file cross-cutting change spanning coderd/, codersdk/, docs/, enterprise/, and site/. Per project convention, omit the scope for cross-cutting changes.
"The SECURITY comment on
IsAuthorizedexplicitly predicted this attack vector. The comment is the canary, and this PR is the coal mine." - Kurapika
🤖 This review was automatically generated with Coder Agents.
3a82a8f to
70b1fea
Compare
ea4601e to
d1181b3
Compare
70b1fea to
65dff84
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Good progress: 12 of 13 R1 findings addressed in a single commit. The CRF-4 fix (key revocation teardown) is well-designed with a controlled-ticker test. The CRF-5 fix (SetReadLimit ordering) correctly resolves a class-of-bug across three call sites. The CRF-8 fix (consecutive failure tracking with log escalation) is a nice touch, though it introduced a bug (CRF-18).
Severity count (new this round): 1 P2, 4 P3. Plus the contested CRF-3 (P1) from R1.
CRF-3 panel vote: 3 reviewers maintain P1 (Mafuuu, Pariston, Kurapika), 1 rates P2 (Knov), rest acknowledge the gap. Universal agreement on one point: deferral without a tracking ticket is a drop. The provisioner daemon analogy understates the risk: provisioner daemons handle infrastructure orchestration, while the AI Bridge path exposes external OAuth tokens for arbitrary users via GetMCPServerAccessTokensBatch. Blocking KeyId-only requests on the network path is a targeted fix with no impact on the in-memory path. This needs a human decision: fix it, file a ticket, or explicitly accept the gap.
"Documentation of a vulnerability is not mitigation." - Pariston
coderd/aibridgedserver/aibridgedserver.go:627
P3 [CRF-20] Inline comment reads: "Trust is established at the in-process transport boundary, not in this RPC." The SECURITY comment at line 596-606 was updated to acknowledge network exposure ("If this RPC is ever exposed over a network boundary... tighten the trust model"). These two comments now contradict each other: one claims in-process-only trust, the other acknowledges network exposure.
Fix: update the inline comment to match the SECURITY block's current state.
(Chopper P3, Razor P3, Knov P2)
🤖
🤖 This review was automatically generated with Coder Agents.
| defer keyCtxCancel() | ||
|
|
||
| // Mark key as used as soon as the request is authenticated. | ||
| if _, err := aiGatewayUpdateKeyLastUsed(keyCtx, api, keyID); err != nil { |
There was a problem hiding this comment.
P3 [CRF-21] aiGatewayUpdateKeyLastUsed runs here before version validation (line 97). A gateway sending an incompatible version gets 400, but the key's last_used_at is already updated. A misconfigured gateway in a connect-fail loop appears "healthy" in the admin dashboard (recent last_used_at) while never establishing a session.
Fix: move the initial liveness update after version validation, or after websocket.Accept.
(Hisoka P3, Razor P3, Knov P2)
🤖
There was a problem hiding this comment.
My assumption was that last_used should show that given key appeared in request headers regardless connection was successful even with API incompatibility or when WebSocket upgrade failed. This shows that something uses this key, even if unsuccessful. API incompatibility will be visible in response and Gateway logs.
Maybe this is wrong and only successful use should be recorded.
| // nolint:gocritic // Recording AI Gateway key liveness is an internal system write. | ||
| rows, err := api.Database.UpdateAIGatewayKeyLastUsedAt(dbauthz.AsSystemRestricted(ctx), keyID) | ||
| if err != nil { | ||
| return true, err |
There was a problem hiding this comment.
P3 [CRF-22] aiGatewayUpdateKeyLastUsed returns (true, err) on database error. The true means "key is active," but the actual state is unknown. In the ticker loop (line 219), this means a deleted key with a persistent DB error is never detected as deleted, because the active flag stays true and ctxCancel() is never called.
Fix: return (false, err) so unknown state is treated as inactive, or handle the error case separately in the caller.
(Mafuuu P3, Knov Nit)
🤖
There was a problem hiding this comment.
This is conscious decision. I believe error when updating last_used should not break existing DRPC session. I'm open to discussion.
There was a problem hiding this comment.
I suggest adding a comment to the func to explain that the bool indicates "active", but if the update func errs then we can't know one way or another, so we assume it to still be active.
4350b16 to
2138d0d
Compare
154ccfc to
156894e
Compare
2138d0d to
754b2bd
Compare
a9ef8a2 to
0241b7a
Compare
156894e to
a3269ec
Compare
0241b7a to
1ff23e2
Compare
a3269ec to
4fd9fb0
Compare
4fd9fb0 to
36e5b2a
Compare
1ff23e2 to
be258ae
Compare
be258ae to
14c2818
Compare

Adds a new enterprise-only
GET /api/v2/ai-gateway/serveendpoint that standalone AI Gateway replicas use to connect tocoderdover a DRPC-over-WebSocket transport, mirroring the existing in-memory path used by the embedded AI Bridge daemon.X-AI-Governance-Gateway-Keyheader is used for authentication.401.aibridged/protoversion (v1.0).400.FeatureAIBridgeentitlement is required.last_used_at) is recorded immediately on connection and refreshed every 60 seconds while the session remains open.Small refactors
The three DRPC service registrations are extracted into
aibridgedserver.Register, shared by both the in-memory and WebSocket paths.The literal
256 * 1024used as the yamux-aligned WebSocket read limit is replaced with the named constantdrpcsdk.YamuxDefaultStreamWindowSizein all call sites.SetReadLimitandWebsocketNetConncalls was fixed.