diff --git a/enterprise/aibridgeproxyd/README.md b/enterprise/aibridgeproxyd/README.md new file mode 100644 index 0000000000..7b9bcf5bc2 --- /dev/null +++ b/enterprise/aibridgeproxyd/README.md @@ -0,0 +1,77 @@ +# AI Bridge Proxy + +A MITM (Man-in-the-Middle) proxy server for intercepting and decrypting HTTPS requests to AI providers. + +## Overview + +The AI Bridge Proxy intercepts HTTPS traffic, decrypts it using a configured CA certificate, and forwards requests to AI Bridge for processing. + +## Configuration + +### Certificate Setup + +Generate a CA key pair for MITM: + +#### 1. Generate a new private key + +```sh +openssl genrsa -out mitm.key 2048 +chmod 400 mitm.key +``` + +#### 2. Create a self-signed CA certificate + +```sh +openssl req -new -x509 -days 365 \ + -key mitm.key \ + -out mitm.crt \ + -subj "/CN=Coder AI Bridge Proxy CA" +``` + +### Configuration options + +| Environment Variable | Description | Default | +|------------------------------------|---------------------------------|---------| +| `CODER_AIBRIDGE_PROXY_ENABLED` | Enable the AI Bridge Proxy | `false` | +| `CODER_AIBRIDGE_PROXY_LISTEN_ADDR` | Address the proxy listens on | `:8888` | +| `CODER_AIBRIDGE_PROXY_CERT_FILE` | Path to the CA certificate file | - | +| `CODER_AIBRIDGE_PROXY_KEY_FILE` | Path to the CA private key file | - | + +### Client Configuration + +Clients must trust the proxy's CA certificate and authenticate with their Coder session token. + +#### CA Certificate + +Clients need to trust the MITM CA certificate: + +```sh +# Node.js +export NODE_EXTRA_CA_CERTS="/path/to/mitm.crt" + +# Python (requests, httpx) +export REQUESTS_CA_BUNDLE="/path/to/mitm.crt" +export SSL_CERT_FILE="/path/to/mitm.crt" + +# Go +export SSL_CERT_FILE="/path/to/mitm.crt" +``` + +#### Proxy Authentication + +Clients authenticate with the proxy using their Coder session token in the `Proxy-Authorization` header via HTTP Basic Auth. +The token is passed as the password (username is ignored): + +```sh +export HTTP_PROXY="http://ignored:@:" +export HTTPS_PROXY="http://ignored:@:" +``` + +For example: + +```sh +export HTTP_PROXY="http://coder:${CODER_SESSION_TOKEN}@localhost:8888" +export HTTPS_PROXY="http://coder:${CODER_SESSION_TOKEN}@localhost:8888" +``` + +Most HTTP clients and AI SDKs will automatically use these environment variables. diff --git a/enterprise/coderd/aibridge.go b/enterprise/coderd/aibridge.go index d1d12d7b02..750b4bfbd5 100644 --- a/enterprise/coderd/aibridge.go +++ b/enterprise/coderd/aibridge.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "strings" "time" "github.com/go-chi/chi/v5" @@ -58,15 +57,7 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f return } - // Strip either the experimental or stable prefix. - // TODO: experimental route is deprecated and must be removed with Beta. - prefixes := []string{"/api/experimental/aibridge", "/api/v2/aibridge"} - for _, prefix := range prefixes { - if strings.Contains(r.URL.String(), prefix) { - http.StripPrefix(prefix, api.aibridgedHandler).ServeHTTP(rw, r) - break - } - } + http.StripPrefix("/api/v2/aibridge", api.aibridgedHandler).ServeHTTP(rw, r) }) }) } diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index d35b166402..db8fc4b7c2 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -674,11 +674,6 @@ func TestAIBridgeRouting(t *testing.T) { path: "/api/v2/aibridge/openai/v1/chat/completions", expectedPath: "/openai/v1/chat/completions", }, - { - name: "ExperimentalPrefix", - path: "/api/experimental/aibridge/openai/v1/chat/completions", - expectedPath: "/openai/v1/chat/completions", - }, } for _, tc := range cases { diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index bf1a5acf53..ad78e72c1d 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -226,12 +226,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { return api.refreshEntitlements(ctx) } - api.AGPL.ExperimentalHandler.Group(func(r chi.Router) { - // Deprecated. - // TODO: remove with Beta release. - r.Route("/aibridge", aibridgeHandler(api, apiKeyMiddleware)) - }) - api.AGPL.APIHandler.Group(func(r chi.Router) { r.Route("/aibridge", aibridgeHandler(api, apiKeyMiddleware)) }) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 656d7137ad..8dd9137890 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2744,6 +2744,16 @@ class ApiMethods { setTimeout(() => res(), 500); }); }; + + getAIBridgeInterceptions = async (options: SearchParamOptions) => { + const url = getURLWithSearchParams( + "/api/v2/aibridge/interceptions", + options, + ); + const response = + await this.axios.get(url); + return response.data; + }; } export type TaskFeedbackRating = "good" | "okay" | "bad"; @@ -2760,16 +2770,6 @@ export type CreateTaskFeedbackRequest = { // above the ApiMethods class for a full explanation. class ExperimentalApiMethods { constructor(protected readonly axios: AxiosInstance) {} - - getAIBridgeInterceptions = async (options: SearchParamOptions) => { - const url = getURLWithSearchParams( - "/api/experimental/aibridge/interceptions", - options, - ); - const response = - await this.axios.get(url); - return response.data; - }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/api/queries/aiBridge.ts b/site/src/api/queries/aiBridge.ts index 1e385bc464..987555aabc 100644 --- a/site/src/api/queries/aiBridge.ts +++ b/site/src/api/queries/aiBridge.ts @@ -13,7 +13,7 @@ export const paginatedInterceptions = ( return ["aiBridgeInterceptions", payload, pageNumber] as const; }, queryFn: ({ limit, offset, payload }) => - API.experimental.getAIBridgeInterceptions({ + API.getAIBridgeInterceptions({ offset, limit, q: payload,