diff --git a/aibridge/bridge_test.go b/aibridge/bridge_test.go index b1339d89cdc..30e2b172d6b 100644 --- a/aibridge/bridge_test.go +++ b/aibridge/bridge_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" + "golang.org/x/xerrors" "cdr.dev/slog/v3/sloggers/slogtest" "github.com/coder/coder/v2/aibridge" @@ -248,21 +249,13 @@ func TestPassthroughRoutesForProviders(t *testing.T) { expectPath: "/v1/models", }, { - name: "copilot_ping", - requestPath: "/copilot/_ping", - provider: func(_ *testing.T, baseURL string) provider.Provider { - return aibridge.NewCopilotProvider(config.Copilot{BaseURL: baseURL}) - }, - expectPath: "/_ping", - }, - { - name: "copilot_auto", + name: "copilot_unknown_route", requestMethod: http.MethodPost, - requestPath: "/copilot/auto", + requestPath: "/copilot/future/endpoint", provider: func(_ *testing.T, baseURL string) provider.Provider { return aibridge.NewCopilotProvider(config.Copilot{BaseURL: baseURL}) }, - expectPath: "/auto", + expectPath: "/future/endpoint", }, } @@ -294,6 +287,46 @@ func TestPassthroughRoutesForProviders(t *testing.T) { } } +func TestBridgedRouteTakesPrecedenceOverPassthroughCatchAll(t *testing.T) { + t.Parallel() + + upstreamCalled := false + interceptorCalled := false + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + upstreamCalled = true + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(upstream.Close) + + // Given: a provider with a bridged route and a passthrough catch-all. + prov := &testutil.MockProvider{ + NameStr: "test", + URL: upstream.URL, + Bridged: []string{"/responses"}, + Passthrough: []string{"/"}, + InterceptorFunc: func(http.ResponseWriter, *http.Request, trace.Tracer) (intercept.Interceptor, error) { + interceptorCalled = true + return nil, xerrors.New("test interceptor error") + }, + } + bridge, err := aibridge.NewRequestBridge( + t.Context(), + []provider.Provider{prov}, + nil, nil, slogtest.Make(t, nil), nil, bridgeTestTracer, + ) + require.NoError(t, err) + + // When: a request targets the bridged route. + req := httptest.NewRequest(http.MethodPost, "/test/responses", nil) + resp := httptest.NewRecorder() + bridge.ServeHTTP(resp, req) + + // Then: the interceptor handles it, not the passthrough upstream. + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.True(t, interceptorCalled) + assert.False(t, upstreamCalled) +} + func TestWebSocketUpgradeRejected(t *testing.T) { t.Parallel() diff --git a/aibridge/provider/copilot.go b/aibridge/provider/copilot.go index 10d634c7bdf..ff05cd0008d 100644 --- a/aibridge/provider/copilot.go +++ b/aibridge/provider/copilot.go @@ -89,16 +89,11 @@ func (*Copilot) BridgedRoutes() []string { } } +// PassthroughRoutes allows all non-bridged routes because Copilot is always +// BYOK. The upstream enforces the user's permissions, so an allowlist is not +// needed to prevent access to privileged operations using shared credentials. func (*Copilot) PassthroughRoutes() []string { - return []string{ - "/_ping", - "/auto", - "/models", - "/models/", - "/agents/", - "/mcp/", - "/.well-known/", - } + return []string{"/"} } func (*Copilot) AuthHeader() string { diff --git a/aibridge/provider/provider.go b/aibridge/provider/provider.go index 7706d83ab11..3935ad55e8d 100644 --- a/aibridge/provider/provider.go +++ b/aibridge/provider/provider.go @@ -75,8 +75,9 @@ type Provider interface { // BridgedRoutes returns a slice of [http.ServeMux]-compatible routes which will have special handling. // See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux. BridgedRoutes() []string - // PassthroughRoutes returns a slice of whitelisted [http.ServeMux]-compatible* routes which are - // not currently intercepted and must be handled by the upstream directly. + // PassthroughRoutes returns a slice of [http.ServeMux]-compatible* routes which are + // not currently intercepted and must be handled by the upstream directly. Providers + // using centralized credentials should restrict these routes to known-safe operations. // // * only path routes can be specified, not ones containing HTTP methods. (i.e. GET /route). // By default, these passthrough routes will accept any HTTP method. diff --git a/docs/ai-coder/ai-gateway/reference.md b/docs/ai-coder/ai-gateway/reference.md index 75fc21c2486..aed6c19cdf2 100644 --- a/docs/ai-coder/ai-gateway/reference.md +++ b/docs/ai-coder/ai-gateway/reference.md @@ -112,12 +112,7 @@ The Anthropic provider also serves the AWS Bedrock provider type. #### Passthrough -- `/models(/*)` -- `/agents/*` -- `/mcp/*` -- `/.well-known/*` - -Any route that is not listed above returns `404`. +All Copilot routes other than the intercepted routes listed above pass through to the configured upstream provider. ## Troubleshooting