From 67307dee8bb26ef64254af3e8396d1b87b59cd09 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 16 Dec 2025 20:01:17 +0000 Subject: [PATCH 1/5] feat: add core AI MITM proxy daemon --- cli/testdata/coder_server_--help.golden | 14 ++ cli/testdata/server-config.yaml.golden | 13 ++ coderd/apidoc/docs.go | 20 +++ coderd/apidoc/swagger.json | 20 +++ codersdk/deployment.go | 55 +++++++ docs/reference/api/general.md | 6 + docs/reference/api/schemas.md | 39 +++++ docs/reference/cli/server.md | 42 +++++ enterprise/aiproxyd/aiproxyd.go | 113 +++++++++++++ enterprise/aiproxyd/aiproxyd_test.go | 152 ++++++++++++++++++ enterprise/cli/aiproxyd.go | 28 ++++ enterprise/cli/server.go | 10 ++ .../cli/testdata/coder_server_--help.golden | 14 ++ go.mod | 1 + go.sum | 2 + site/src/api/typesGenerated.ts | 9 ++ 16 files changed, 538 insertions(+) create mode 100644 enterprise/aiproxyd/aiproxyd.go create mode 100644 enterprise/aiproxyd/aiproxyd_test.go create mode 100644 enterprise/cli/aiproxyd.go diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 43ed94360164e..13012c650f78e 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -139,6 +139,20 @@ AI BRIDGE OPTIONS: Maximum number of AI Bridge requests per second per replica. Set to 0 to disable (unlimited). +AI PROXY OPTIONS: + --aiproxy-cert-file string, $CODER_AIPROXY_CERT_FILE + Path to the CA certificate file for MITM. + + --aiproxy-enabled bool, $CODER_AIPROXY_ENABLED (default: false) + Enable the AI MITM proxy for intercepting and decrypting AI provider + requests. + + --aiproxy-key-file string, $CODER_AIPROXY_KEY_FILE + Path to the CA private key file for MITM. + + --aiproxy-listen-addr string, $CODER_AIPROXY_LISTEN_ADDR (default: :8888) + The address the AI proxy will listen on. + CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 14ea079f4cec6..70492148b32a9 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -765,6 +765,19 @@ aibridge: # (unlimited). # (default: 0, type: int) rateLimit: 0 +aiproxy: + # Enable the AI MITM proxy for intercepting and decrypting AI provider requests. + # (default: false, type: bool) + enabled: false + # The address the AI proxy will listen on. + # (default: :8888, type: string) + listen_addr: :8888 + # Path to the CA certificate file for MITM. + # (default: , type: string) + cert_file: "" + # Path to the CA private key file for MITM. + # (default: , type: string) + key_file: "" # Configure data retention policies for various database tables. Retention # policies automatically purge old data to reduce database size and improve # performance. Setting a retention duration to 0 disables automatic purging for diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0bb555de96c44..3669d179a452f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12074,6 +12074,26 @@ const docTemplate = `{ "properties": { "bridge": { "$ref": "#/definitions/codersdk.AIBridgeConfig" + }, + "proxy": { + "$ref": "#/definitions/codersdk.AIProxyConfig" + } + } + }, + "codersdk.AIProxyConfig": { + "type": "object", + "properties": { + "cert_file": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "key_file": { + "type": "string" + }, + "listen_addr": { + "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index aa4af943bfca3..14ab4e140a9a5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10740,6 +10740,26 @@ "properties": { "bridge": { "$ref": "#/definitions/codersdk.AIBridgeConfig" + }, + "proxy": { + "$ref": "#/definitions/codersdk.AIProxyConfig" + } + } + }, + "codersdk.AIProxyConfig": { + "type": "object", + "properties": { + "cert_file": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "key_file": { + "type": "string" + }, + "listen_addr": { + "type": "string" } } }, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 58725eea8456d..2cb4eeed2864f 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1217,6 +1217,10 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Name: "AI Bridge", YAML: "aibridge", } + deploymentGroupAIProxy = serpent.Group{ + Name: "AI Proxy", + YAML: "aiproxy", + } deploymentGroupRetention = serpent.Group{ Name: "Retention", Description: "Configure data retention policies for various database tables. Retention policies automatically purge old data to reduce database size and improve performance. Setting a retention duration to 0 disables automatic purging for that data type.", @@ -3443,6 +3447,49 @@ Write out the current server config as YAML to stdout.`, Group: &deploymentGroupAIBridge, YAML: "rateLimit", }, + + // AI Proxy Options + { + Name: "AI Proxy Enabled", + Description: "Enable the AI MITM proxy for intercepting and decrypting AI provider requests.", + Flag: "aiproxy-enabled", + Env: "CODER_AIPROXY_ENABLED", + Value: &c.AI.ProxyConfig.Enabled, + Default: "false", + Group: &deploymentGroupAIProxy, + YAML: "enabled", + }, + { + Name: "AI Proxy Listen Address", + Description: "The address the AI proxy will listen on.", + Flag: "aiproxy-listen-addr", + Env: "CODER_AIPROXY_LISTEN_ADDR", + Value: &c.AI.ProxyConfig.ListenAddr, + Default: ":8888", + Group: &deploymentGroupAIProxy, + YAML: "listen_addr", + }, + { + Name: "AI Proxy Certificate File", + Description: "Path to the CA certificate file for MITM.", + Flag: "aiproxy-cert-file", + Env: "CODER_AIPROXY_CERT_FILE", + Value: &c.AI.ProxyConfig.CertFile, + Default: "", + Group: &deploymentGroupAIProxy, + YAML: "cert_file", + }, + { + Name: "AI Proxy Key File", + Description: "Path to the CA private key file for MITM.", + Flag: "aiproxy-key-file", + Env: "CODER_AIPROXY_KEY_FILE", + Value: &c.AI.ProxyConfig.KeyFile, + Default: "", + Group: &deploymentGroupAIProxy, + YAML: "key_file", + }, + // Retention settings { Name: "Audit Logs Retention", @@ -3535,8 +3582,16 @@ type AIBridgeBedrockConfig struct { SmallFastModel serpent.String `json:"small_fast_model" typescript:",notnull"` } +type AIProxyConfig struct { + Enabled serpent.Bool `json:"enabled" typescript:",notnull"` + ListenAddr serpent.String `json:"listen_addr" typescript:",notnull"` + CertFile serpent.String `json:"cert_file" typescript:",notnull"` + KeyFile serpent.String `json:"key_file" typescript:",notnull"` +} + type AIConfig struct { BridgeConfig AIBridgeConfig `json:"bridge,omitempty"` + ProxyConfig AIProxyConfig `json:"proxy,omitempty"` } type SupportConfig struct { diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index e9a12a196ffd6..da40a73801ff0 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -183,6 +183,12 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ }, "rate_limit": 0, "retention": 0 + }, + "proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" } }, "allow_workspace_renames": true, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ca609d0ac4842..118baaea6917b 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -711,6 +711,12 @@ }, "rate_limit": 0, "retention": 0 + }, + "proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" } } ``` @@ -720,6 +726,27 @@ | Name | Type | Required | Restrictions | Description | |----------|----------------------------------------------------|----------|--------------|-------------| | `bridge` | [codersdk.AIBridgeConfig](#codersdkaibridgeconfig) | false | | | +| `proxy` | [codersdk.AIProxyConfig](#codersdkaiproxyconfig) | false | | | + +## codersdk.AIProxyConfig + +```json +{ + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------------|---------|----------|--------------|-------------| +| `cert_file` | string | false | | | +| `enabled` | boolean | false | | | +| `key_file` | string | false | | | +| `listen_addr` | string | false | | | ## codersdk.APIAllowListTarget @@ -2873,6 +2900,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "rate_limit": 0, "retention": 0 + }, + "proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" } }, "allow_workspace_renames": true, @@ -3404,6 +3437,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "rate_limit": 0, "retention": 0 + }, + "proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" } }, "allow_workspace_renames": true, diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 6f7aa705a9b6a..a1d4e732f7367 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1814,6 +1814,48 @@ Maximum number of concurrent AI Bridge requests per replica. Set to 0 to disable Maximum number of AI Bridge requests per second per replica. Set to 0 to disable (unlimited). +### --aiproxy-enabled + +| | | +|-------------|-------------------------------------| +| Type | bool | +| Environment | $CODER_AIPROXY_ENABLED | +| YAML | aiproxy.enabled | +| Default | false | + +Enable the AI MITM proxy for intercepting and decrypting AI provider requests. + +### --aiproxy-listen-addr + +| | | +|-------------|-----------------------------------------| +| Type | string | +| Environment | $CODER_AIPROXY_LISTEN_ADDR | +| YAML | aiproxy.listen_addr | +| Default | :8888 | + +The address the AI proxy will listen on. + +### --aiproxy-cert-file + +| | | +|-------------|---------------------------------------| +| Type | string | +| Environment | $CODER_AIPROXY_CERT_FILE | +| YAML | aiproxy.cert_file | + +Path to the CA certificate file for MITM. + +### --aiproxy-key-file + +| | | +|-------------|--------------------------------------| +| Type | string | +| Environment | $CODER_AIPROXY_KEY_FILE | +| YAML | aiproxy.key_file | + +Path to the CA private key file for MITM. + ### --audit-logs-retention | | | diff --git a/enterprise/aiproxyd/aiproxyd.go b/enterprise/aiproxyd/aiproxyd.go new file mode 100644 index 0000000000000..2ea856ba681dc --- /dev/null +++ b/enterprise/aiproxyd/aiproxyd.go @@ -0,0 +1,113 @@ +package aiproxyd + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "net/http" + "time" + + "github.com/elazarl/goproxy" + "golang.org/x/xerrors" + + "cdr.dev/slog" +) + +// Server is the AI MITM (Man-in-the-Middle) proxy server. +// It is responsible for: +// - intercepting HTTPS requests to AI providers +// - decrypting requests using the configured CA certificate +// - forwarding requests to aibridge for processing +type Server struct { + logger slog.Logger + proxy *goproxy.ProxyHttpServer + httpServer *http.Server +} + +// Options configures the AI proxy server. +type Options struct { + // ListenAddr is the address the proxy server will listen on. + ListenAddr string + // CertFile is the path to the CA certificate file used for MITM. + CertFile string + // KeyFile is the path to the CA private key file used for MITM. + KeyFile string +} + +func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error) { + logger.Info(ctx, "initializing AI proxy server") + + if opts.ListenAddr == "" { + return nil, xerrors.New("listen address is required") + } + + if opts.CertFile == "" || opts.KeyFile == "" { + return nil, xerrors.New("cert file and key file are required") + } + + // Load CA certificate for MITM + if err := loadMitmCertificate(opts.CertFile, opts.KeyFile); err != nil { + return nil, xerrors.Errorf("failed to load MITM certificate: %w", err) + } + + proxy := goproxy.NewProxyHttpServer() + + // Decrypt all HTTPS requests via MITM. Requests are forwarded to + // the original destination without modification for now. + // TODO(ssncferreira): Route requests to aibridged + // See https://github.com/coder/internal/issues/1181 + proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) + + srv := &Server{ + logger: logger, + proxy: proxy, + } + + // Start HTTP server in background + srv.httpServer = &http.Server{ + Addr: opts.ListenAddr, + Handler: proxy, + ReadHeaderTimeout: 10 * time.Second, + } + + go func() { + logger.Info(ctx, "starting AI proxy", slog.F("addr", opts.ListenAddr)) + if err := srv.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error(ctx, "aiproxyd server error", slog.Error(err)) + } + }() + + return srv, nil +} + +// loadMitmCertificate loads the CA certificate and key for MITM into goproxy. +func loadMitmCertificate(certFile, keyFile string) error { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return xerrors.Errorf("load x509 keypair: %w", err) + } + + x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return xerrors.Errorf("parse certificate: %w", err) + } + + goproxy.GoproxyCa = tls.Certificate{ + Certificate: tlsCert.Certificate, + PrivateKey: tlsCert.PrivateKey, + Leaf: x509Cert, + } + + return nil +} + +// Close gracefully shuts down the proxy server. +func (s *Server) Close() error { + if s.httpServer == nil { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return s.httpServer.Shutdown(ctx) +} diff --git a/enterprise/aiproxyd/aiproxyd_test.go b/enterprise/aiproxyd/aiproxyd_test.go new file mode 100644 index 0000000000000..005518f418c89 --- /dev/null +++ b/enterprise/aiproxyd/aiproxyd_test.go @@ -0,0 +1,152 @@ +package aiproxyd_test + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cdr.dev/slog/sloggers/slogtest" + + "github.com/coder/coder/v2/enterprise/aiproxyd" +) + +// generateTestCA creates a temporary CA certificate and key for testing. +func generateTestCA(t *testing.T) (certFile, keyFile string) { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey) + require.NoError(t, err) + + tempDir := t.TempDir() + certFile = filepath.Join(tempDir, "ca.crt") + keyFile = filepath.Join(tempDir, "ca.key") + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + err = os.WriteFile(certFile, certPEM, 0o600) + require.NoError(t, err) + + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + err = os.WriteFile(keyFile, keyPEM, 0o600) + require.NoError(t, err) + + return certFile, keyFile +} + +func TestNew(t *testing.T) { + t.Parallel() + + t.Run("MissingListenAddr", func(t *testing.T) { + t.Parallel() + + certFile, keyFile := generateTestCA(t) + logger := slogtest.Make(t, nil) + + _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: "", + CertFile: certFile, + KeyFile: keyFile, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "listen address is required") + }) + + t.Run("MissingCertFile", func(t *testing.T) { + t.Parallel() + logger := slogtest.Make(t, nil) + + _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: ":0", + KeyFile: "key.pem", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "cert file and key file are required") + }) + + t.Run("MissingKeyFile", func(t *testing.T) { + t.Parallel() + logger := slogtest.Make(t, nil) + + _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: ":0", + CertFile: "cert.pem", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "cert file and key file are required") + }) + + t.Run("InvalidCertFile", func(t *testing.T) { + t.Parallel() + logger := slogtest.Make(t, nil) + + _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: ":0", + CertFile: "/nonexistent/cert.pem", + KeyFile: "/nonexistent/key.pem", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to load MITM certificate") + }) + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + certFile, keyFile := generateTestCA(t) + logger := slogtest.Make(t, nil) + + srv, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: "127.0.0.1:0", + CertFile: certFile, + KeyFile: keyFile, + }) + require.NoError(t, err) + require.NotNil(t, srv) + + err = srv.Close() + require.NoError(t, err) + }) +} + +func TestClose(t *testing.T) { + t.Parallel() + + certFile, keyFile := generateTestCA(t) + logger := slogtest.Make(t, nil) + + srv, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: "127.0.0.1:0", + CertFile: certFile, + KeyFile: keyFile, + }) + require.NoError(t, err) + + err = srv.Close() + require.NoError(t, err) + + // Calling Close again should not error + err = srv.Close() + require.NoError(t, err) +} diff --git a/enterprise/cli/aiproxyd.go b/enterprise/cli/aiproxyd.go new file mode 100644 index 0000000000000..719148b4ef249 --- /dev/null +++ b/enterprise/cli/aiproxyd.go @@ -0,0 +1,28 @@ +//go:build !slim + +package cli + +import ( + "context" + + "github.com/coder/coder/v2/enterprise/aiproxyd" + "github.com/coder/coder/v2/enterprise/coderd" +) + +func newAIProxyDaemon(coderAPI *coderd.API) (*aiproxyd.Server, error) { + ctx := context.Background() + coderAPI.Logger.Debug(ctx, "starting in-memory aiproxy daemon") + + logger := coderAPI.Logger.Named("aiproxyd") + + srv, err := aiproxyd.New(ctx, logger, aiproxyd.Options{ + ListenAddr: coderAPI.DeploymentValues.AI.ProxyConfig.ListenAddr.String(), + CertFile: coderAPI.DeploymentValues.AI.ProxyConfig.CertFile.String(), + KeyFile: coderAPI.DeploymentValues.AI.ProxyConfig.KeyFile.String(), + }) + if err != nil { + return nil, err + } + + return srv, nil +} diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index bc77bc54ba522..c4686bab52510 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -165,6 +165,16 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { closers.Add(aibridgeDaemon) } + // In-memory AI proxy daemon + if options.DeploymentValues.AI.ProxyConfig.Enabled.Value() { + aiProxyServer, err := newAIProxyDaemon(api) + if err != nil { + _ = closers.Close() + return nil, nil, xerrors.Errorf("create aiproxyd: %w", err) + } + closers.Add(aiProxyServer) + } + return api.AGPL, closers, nil }) diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 20c3c2609e97e..7aa7866085d10 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -140,6 +140,20 @@ AI BRIDGE OPTIONS: Maximum number of AI Bridge requests per second per replica. Set to 0 to disable (unlimited). +AI PROXY OPTIONS: + --aiproxy-cert-file string, $CODER_AIPROXY_CERT_FILE + Path to the CA certificate file for MITM. + + --aiproxy-enabled bool, $CODER_AIPROXY_ENABLED (default: false) + Enable the AI MITM proxy for intercepting and decrypting AI provider + requests. + + --aiproxy-key-file string, $CODER_AIPROXY_KEY_FILE + Path to the CA private key file for MITM. + + --aiproxy-listen-addr string, $CODER_AIPROXY_LISTEN_ADDR (default: :8888) + The address the AI proxy will listen on. + CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. diff --git a/go.mod b/go.mod index d9ff4d9150bee..7de96272d8197 100644 --- a/go.mod +++ b/go.mod @@ -482,6 +482,7 @@ require ( github.com/coder/preview v1.0.4 github.com/danieljoos/wincred v1.2.3 github.com/dgraph-io/ristretto/v2 v2.3.0 + github.com/elazarl/goproxy v1.7.2 github.com/fsnotify/fsnotify v1.9.0 github.com/go-git/go-git/v5 v5.16.2 github.com/icholy/replace v0.6.0 diff --git a/go.sum b/go.sum index 5fde858653f4a..05dad52f00c1e 100644 --- a/go.sum +++ b/go.sum @@ -1041,6 +1041,8 @@ github.com/elastic/go-sysinfo v1.15.1 h1:zBmTnFEXxIQ3iwcQuk7MzaUotmKRp3OabbbWM8T github.com/elastic/go-sysinfo v1.15.1/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk= github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.21.2 h1:OLDgvZKuofk4em9fT5tFG5j4jE1/hXnX75UMvcrL4AA= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 93dee1e18f4b9..814e88ae609ca 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -106,6 +106,15 @@ export interface AIBridgeUserPrompt { // From codersdk/deployment.go export interface AIConfig { readonly bridge?: AIBridgeConfig; + readonly proxy?: AIProxyConfig; +} + +// From codersdk/deployment.go +export interface AIProxyConfig { + readonly enabled: boolean; + readonly listen_addr: string; + readonly cert_file: string; + readonly key_file: string; } // From codersdk/allowlist.go From c1825d6ff512584a96c12f3cdec1ac541b46f425 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 17 Dec 2025 12:56:04 +0000 Subject: [PATCH 2/5] chore: address comments --- enterprise/aiproxyd/aiproxyd_test.go | 76 ++++++++++++++++++++++++++++ enterprise/cli/aiproxyd.go | 4 +- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/enterprise/aiproxyd/aiproxyd_test.go b/enterprise/aiproxyd/aiproxyd_test.go index 005518f418c89..8330ffb1cf733 100644 --- a/enterprise/aiproxyd/aiproxyd_test.go +++ b/enterprise/aiproxyd/aiproxyd_test.go @@ -3,10 +3,16 @@ package aiproxyd_test import ( "crypto/rand" "crypto/rsa" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "io" "math/big" + "net" + "net/http" + "net/http/httptest" + "net/url" "os" "path/filepath" "testing" @@ -17,6 +23,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/enterprise/aiproxyd" + "github.com/coder/coder/v2/testutil" ) // generateTestCA creates a temporary CA certificate and key for testing. @@ -76,6 +83,7 @@ func TestNew(t *testing.T) { t.Run("MissingCertFile", func(t *testing.T) { t.Parallel() + logger := slogtest.Make(t, nil) _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ @@ -88,6 +96,7 @@ func TestNew(t *testing.T) { t.Run("MissingKeyFile", func(t *testing.T) { t.Parallel() + logger := slogtest.Make(t, nil) _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ @@ -100,6 +109,7 @@ func TestNew(t *testing.T) { t.Run("InvalidCertFile", func(t *testing.T) { t.Parallel() + logger := slogtest.Make(t, nil) _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ @@ -150,3 +160,69 @@ func TestClose(t *testing.T) { err = srv.Close() require.NoError(t, err) } + +func TestProxy_MITM(t *testing.T) { + t.Parallel() + + // Create a mock HTTPS server that will be the target of our proxied request. + targetServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("hello from target")) + })) + defer targetServer.Close() + + certFile, keyFile := generateTestCA(t) + logger := slogtest.Make(t, nil) + + // Start the proxy server. + srv, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + ListenAddr: "127.0.0.1:8888", + CertFile: certFile, + KeyFile: keyFile, + }) + require.NoError(t, err) + t.Cleanup(func() { _ = srv.Close() }) + + // Wait for the proxy server to be ready. + require.Eventually(t, func() bool { + conn, err := net.Dial("tcp", "127.0.0.1:8888") + if err != nil { + return false + } + _ = conn.Close() + return true + }, testutil.WaitShort, testutil.IntervalFast) + + // Load the CA certificate so the client trusts the proxy's MITM certificate. + certPEM, err := os.ReadFile(certFile) + require.NoError(t, err) + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(certPEM) + + // Create an HTTP client configured to use the proxy. + proxyURL, err := url.Parse("http://127.0.0.1:8888") + require.NoError(t, err) + + client := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: certPool, + }, + }, + } + + // Make a request through the proxy to the target server. + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, targetServer.URL, nil) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // Verify the response was successfully proxied. + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, "hello from target", string(body)) +} diff --git a/enterprise/cli/aiproxyd.go b/enterprise/cli/aiproxyd.go index 719148b4ef249..c1411dcf09e43 100644 --- a/enterprise/cli/aiproxyd.go +++ b/enterprise/cli/aiproxyd.go @@ -5,6 +5,8 @@ package cli import ( "context" + "golang.org/x/xerrors" + "github.com/coder/coder/v2/enterprise/aiproxyd" "github.com/coder/coder/v2/enterprise/coderd" ) @@ -21,7 +23,7 @@ func newAIProxyDaemon(coderAPI *coderd.API) (*aiproxyd.Server, error) { KeyFile: coderAPI.DeploymentValues.AI.ProxyConfig.KeyFile.String(), }) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to start in-memory aiproxy daemon: %w", err) } return srv, nil From 1651bbe17bbd412c2c5a4b5ba1cedc50eb5f97ef Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 17 Dec 2025 13:27:05 +0000 Subject: [PATCH 3/5] chore: rename aiproxyd to aibridgeproxyd --- cli/testdata/coder_server_--help.golden | 10 +-- cli/testdata/server-config.yaml.golden | 2 +- coderd/apidoc/docs.go | 40 ++++----- coderd/apidoc/swagger.json | 40 ++++----- codersdk/deployment.go | 54 ++++++------ docs/reference/api/general.md | 12 +-- docs/reference/api/schemas.md | 84 +++++++++---------- docs/reference/cli/server.md | 52 ++++++------ .../aibridgeproxyd.go} | 4 +- .../aibridgeproxyd_test.go} | 18 ++-- enterprise/cli/aibridgeproxyd.go | 30 +++++++ enterprise/cli/aiproxyd.go | 30 ------- enterprise/cli/server.go | 6 +- .../cli/testdata/coder_server_--help.golden | 10 +-- site/src/api/typesGenerated.ts | 18 ++-- 15 files changed, 205 insertions(+), 205 deletions(-) rename enterprise/{aiproxyd/aiproxyd.go => aibridgeproxyd/aibridgeproxyd.go} (96%) rename enterprise/{aiproxyd/aiproxyd_test.go => aibridgeproxyd/aibridgeproxyd_test.go} (89%) create mode 100644 enterprise/cli/aibridgeproxyd.go delete mode 100644 enterprise/cli/aiproxyd.go diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 13012c650f78e..bdbd5076e9d6f 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -139,18 +139,18 @@ AI BRIDGE OPTIONS: Maximum number of AI Bridge requests per second per replica. Set to 0 to disable (unlimited). -AI PROXY OPTIONS: - --aiproxy-cert-file string, $CODER_AIPROXY_CERT_FILE +AI BRIDGE PROXY OPTIONS: + --aibridge-proxy-cert-file string, $CODER_AIBRIDGE_PROXY_CERT_FILE Path to the CA certificate file for MITM. - --aiproxy-enabled bool, $CODER_AIPROXY_ENABLED (default: false) + --aibridge-proxy-enabled bool, $CODER_AIBRIDGE_PROXY_ENABLED (default: false) Enable the AI MITM proxy for intercepting and decrypting AI provider requests. - --aiproxy-key-file string, $CODER_AIPROXY_KEY_FILE + --aibridge-proxy-key-file string, $CODER_AIBRIDGE_PROXY_KEY_FILE Path to the CA private key file for MITM. - --aiproxy-listen-addr string, $CODER_AIPROXY_LISTEN_ADDR (default: :8888) + --aibridge-proxy-listen-addr string, $CODER_AIBRIDGE_PROXY_LISTEN_ADDR (default: :8888) The address the AI proxy will listen on. CLIENT OPTIONS: diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 70492148b32a9..9181b9cb4c561 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -765,7 +765,7 @@ aibridge: # (unlimited). # (default: 0, type: int) rateLimit: 0 -aiproxy: +aibridgeproxy: # Enable the AI MITM proxy for intercepting and decrypting AI provider requests. # (default: false, type: bool) enabled: false diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3669d179a452f..f74d7542b1732 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11973,6 +11973,23 @@ const docTemplate = `{ } } }, + "codersdk.AIBridgeProxyConfig": { + "type": "object", + "properties": { + "cert_file": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "key_file": { + "type": "string" + }, + "listen_addr": { + "type": "string" + } + } + }, "codersdk.AIBridgeTokenUsage": { "type": "object", "properties": { @@ -12072,28 +12089,11 @@ const docTemplate = `{ "codersdk.AIConfig": { "type": "object", "properties": { + "aibridge_proxy": { + "$ref": "#/definitions/codersdk.AIBridgeProxyConfig" + }, "bridge": { "$ref": "#/definitions/codersdk.AIBridgeConfig" - }, - "proxy": { - "$ref": "#/definitions/codersdk.AIProxyConfig" - } - } - }, - "codersdk.AIProxyConfig": { - "type": "object", - "properties": { - "cert_file": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "key_file": { - "type": "string" - }, - "listen_addr": { - "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 14ab4e140a9a5..effcd7419bd17 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10639,6 +10639,23 @@ } } }, + "codersdk.AIBridgeProxyConfig": { + "type": "object", + "properties": { + "cert_file": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "key_file": { + "type": "string" + }, + "listen_addr": { + "type": "string" + } + } + }, "codersdk.AIBridgeTokenUsage": { "type": "object", "properties": { @@ -10738,28 +10755,11 @@ "codersdk.AIConfig": { "type": "object", "properties": { + "aibridge_proxy": { + "$ref": "#/definitions/codersdk.AIBridgeProxyConfig" + }, "bridge": { "$ref": "#/definitions/codersdk.AIBridgeConfig" - }, - "proxy": { - "$ref": "#/definitions/codersdk.AIProxyConfig" - } - } - }, - "codersdk.AIProxyConfig": { - "type": "object", - "properties": { - "cert_file": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "key_file": { - "type": "string" - }, - "listen_addr": { - "type": "string" } } }, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 2cb4eeed2864f..1d9f2f04704ed 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1217,9 +1217,9 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Name: "AI Bridge", YAML: "aibridge", } - deploymentGroupAIProxy = serpent.Group{ - Name: "AI Proxy", - YAML: "aiproxy", + deploymentGroupAIBridgeProxy = serpent.Group{ + Name: "AI Bridge Proxy", + YAML: "aibridgeproxy", } deploymentGroupRetention = serpent.Group{ Name: "Retention", @@ -3448,45 +3448,45 @@ Write out the current server config as YAML to stdout.`, YAML: "rateLimit", }, - // AI Proxy Options + // AI Bridge Proxy Options { - Name: "AI Proxy Enabled", + Name: "AI Bridge Proxy Enabled", Description: "Enable the AI MITM proxy for intercepting and decrypting AI provider requests.", - Flag: "aiproxy-enabled", - Env: "CODER_AIPROXY_ENABLED", - Value: &c.AI.ProxyConfig.Enabled, + Flag: "aibridge-proxy-enabled", + Env: "CODER_AIBRIDGE_PROXY_ENABLED", + Value: &c.AI.BridgeProxyConfig.Enabled, Default: "false", - Group: &deploymentGroupAIProxy, + Group: &deploymentGroupAIBridgeProxy, YAML: "enabled", }, { - Name: "AI Proxy Listen Address", + Name: "AI Bridge Proxy Listen Address", Description: "The address the AI proxy will listen on.", - Flag: "aiproxy-listen-addr", - Env: "CODER_AIPROXY_LISTEN_ADDR", - Value: &c.AI.ProxyConfig.ListenAddr, + Flag: "aibridge-proxy-listen-addr", + Env: "CODER_AIBRIDGE_PROXY_LISTEN_ADDR", + Value: &c.AI.BridgeProxyConfig.ListenAddr, Default: ":8888", - Group: &deploymentGroupAIProxy, + Group: &deploymentGroupAIBridgeProxy, YAML: "listen_addr", }, { - Name: "AI Proxy Certificate File", + Name: "AI Bridge Proxy Certificate File", Description: "Path to the CA certificate file for MITM.", - Flag: "aiproxy-cert-file", - Env: "CODER_AIPROXY_CERT_FILE", - Value: &c.AI.ProxyConfig.CertFile, + Flag: "aibridge-proxy-cert-file", + Env: "CODER_AIBRIDGE_PROXY_CERT_FILE", + Value: &c.AI.BridgeProxyConfig.CertFile, Default: "", - Group: &deploymentGroupAIProxy, + Group: &deploymentGroupAIBridgeProxy, YAML: "cert_file", }, { - Name: "AI Proxy Key File", + Name: "AI Bridge Proxy Key File", Description: "Path to the CA private key file for MITM.", - Flag: "aiproxy-key-file", - Env: "CODER_AIPROXY_KEY_FILE", - Value: &c.AI.ProxyConfig.KeyFile, + Flag: "aibridge-proxy-key-file", + Env: "CODER_AIBRIDGE_PROXY_KEY_FILE", + Value: &c.AI.BridgeProxyConfig.KeyFile, Default: "", - Group: &deploymentGroupAIProxy, + Group: &deploymentGroupAIBridgeProxy, YAML: "key_file", }, @@ -3582,7 +3582,7 @@ type AIBridgeBedrockConfig struct { SmallFastModel serpent.String `json:"small_fast_model" typescript:",notnull"` } -type AIProxyConfig struct { +type AIBridgeProxyConfig struct { Enabled serpent.Bool `json:"enabled" typescript:",notnull"` ListenAddr serpent.String `json:"listen_addr" typescript:",notnull"` CertFile serpent.String `json:"cert_file" typescript:",notnull"` @@ -3590,8 +3590,8 @@ type AIProxyConfig struct { } type AIConfig struct { - BridgeConfig AIBridgeConfig `json:"bridge,omitempty"` - ProxyConfig AIProxyConfig `json:"proxy,omitempty"` + BridgeConfig AIBridgeConfig `json:"bridge,omitempty"` + BridgeProxyConfig AIBridgeProxyConfig `json:"aibridge_proxy,omitempty"` } type SupportConfig struct { diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index da40a73801ff0..83dae06688331 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -162,6 +162,12 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ }, "agent_stat_refresh_interval": 0, "ai": { + "aibridge_proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" + }, "bridge": { "anthropic": { "base_url": "string", @@ -183,12 +189,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ }, "rate_limit": 0, "retention": 0 - }, - "proxy": { - "cert_file": "string", - "enabled": true, - "key_file": "string", - "listen_addr": "string" } }, "allow_workspace_renames": true, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 118baaea6917b..328135b6c0a18 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -592,6 +592,26 @@ | `base_url` | string | false | | | | `key` | string | false | | | +## codersdk.AIBridgeProxyConfig + +```json +{ + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------------|---------|----------|--------------|-------------| +| `cert_file` | string | false | | | +| `enabled` | boolean | false | | | +| `key_file` | string | false | | | +| `listen_addr` | string | false | | | + ## codersdk.AIBridgeTokenUsage ```json @@ -690,6 +710,12 @@ ```json { + "aibridge_proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" + }, "bridge": { "anthropic": { "base_url": "string", @@ -711,42 +737,16 @@ }, "rate_limit": 0, "retention": 0 - }, - "proxy": { - "cert_file": "string", - "enabled": true, - "key_file": "string", - "listen_addr": "string" } } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|----------|----------------------------------------------------|----------|--------------|-------------| -| `bridge` | [codersdk.AIBridgeConfig](#codersdkaibridgeconfig) | false | | | -| `proxy` | [codersdk.AIProxyConfig](#codersdkaiproxyconfig) | false | | | - -## codersdk.AIProxyConfig - -```json -{ - "cert_file": "string", - "enabled": true, - "key_file": "string", - "listen_addr": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|---------------|---------|----------|--------------|-------------| -| `cert_file` | string | false | | | -| `enabled` | boolean | false | | | -| `key_file` | string | false | | | -| `listen_addr` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------|--------------------------------------------------------------|----------|--------------|-------------| +| `aibridge_proxy` | [codersdk.AIBridgeProxyConfig](#codersdkaibridgeproxyconfig) | false | | | +| `bridge` | [codersdk.AIBridgeConfig](#codersdkaibridgeconfig) | false | | | ## codersdk.APIAllowListTarget @@ -2879,6 +2879,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "agent_stat_refresh_interval": 0, "ai": { + "aibridge_proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" + }, "bridge": { "anthropic": { "base_url": "string", @@ -2900,12 +2906,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "rate_limit": 0, "retention": 0 - }, - "proxy": { - "cert_file": "string", - "enabled": true, - "key_file": "string", - "listen_addr": "string" } }, "allow_workspace_renames": true, @@ -3416,6 +3416,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "agent_stat_refresh_interval": 0, "ai": { + "aibridge_proxy": { + "cert_file": "string", + "enabled": true, + "key_file": "string", + "listen_addr": "string" + }, "bridge": { "anthropic": { "base_url": "string", @@ -3437,12 +3443,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "rate_limit": 0, "retention": 0 - }, - "proxy": { - "cert_file": "string", - "enabled": true, - "key_file": "string", - "listen_addr": "string" } }, "allow_workspace_renames": true, diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index a1d4e732f7367..6a9d718295658 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1814,45 +1814,45 @@ Maximum number of concurrent AI Bridge requests per replica. Set to 0 to disable Maximum number of AI Bridge requests per second per replica. Set to 0 to disable (unlimited). -### --aiproxy-enabled +### --aibridge-proxy-enabled -| | | -|-------------|-------------------------------------| -| Type | bool | -| Environment | $CODER_AIPROXY_ENABLED | -| YAML | aiproxy.enabled | -| Default | false | +| | | +|-------------|--------------------------------------------| +| Type | bool | +| Environment | $CODER_AIBRIDGE_PROXY_ENABLED | +| YAML | aibridgeproxy.enabled | +| Default | false | Enable the AI MITM proxy for intercepting and decrypting AI provider requests. -### --aiproxy-listen-addr +### --aibridge-proxy-listen-addr -| | | -|-------------|-----------------------------------------| -| Type | string | -| Environment | $CODER_AIPROXY_LISTEN_ADDR | -| YAML | aiproxy.listen_addr | -| Default | :8888 | +| | | +|-------------|------------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_PROXY_LISTEN_ADDR | +| YAML | aibridgeproxy.listen_addr | +| Default | :8888 | The address the AI proxy will listen on. -### --aiproxy-cert-file +### --aibridge-proxy-cert-file -| | | -|-------------|---------------------------------------| -| Type | string | -| Environment | $CODER_AIPROXY_CERT_FILE | -| YAML | aiproxy.cert_file | +| | | +|-------------|----------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_PROXY_CERT_FILE | +| YAML | aibridgeproxy.cert_file | Path to the CA certificate file for MITM. -### --aiproxy-key-file +### --aibridge-proxy-key-file -| | | -|-------------|--------------------------------------| -| Type | string | -| Environment | $CODER_AIPROXY_KEY_FILE | -| YAML | aiproxy.key_file | +| | | +|-------------|---------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_PROXY_KEY_FILE | +| YAML | aibridgeproxy.key_file | Path to the CA private key file for MITM. diff --git a/enterprise/aiproxyd/aiproxyd.go b/enterprise/aibridgeproxyd/aibridgeproxyd.go similarity index 96% rename from enterprise/aiproxyd/aiproxyd.go rename to enterprise/aibridgeproxyd/aibridgeproxyd.go index 2ea856ba681dc..8db4bcc4e6855 100644 --- a/enterprise/aiproxyd/aiproxyd.go +++ b/enterprise/aibridgeproxyd/aibridgeproxyd.go @@ -1,4 +1,4 @@ -package aiproxyd +package aibridgeproxyd import ( "context" @@ -74,7 +74,7 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error) go func() { logger.Info(ctx, "starting AI proxy", slog.F("addr", opts.ListenAddr)) if err := srv.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - logger.Error(ctx, "aiproxyd server error", slog.Error(err)) + logger.Error(ctx, "aibridgeproxyd server error", slog.Error(err)) } }() diff --git a/enterprise/aiproxyd/aiproxyd_test.go b/enterprise/aibridgeproxyd/aibridgeproxyd_test.go similarity index 89% rename from enterprise/aiproxyd/aiproxyd_test.go rename to enterprise/aibridgeproxyd/aibridgeproxyd_test.go index 8330ffb1cf733..5efc31e3dface 100644 --- a/enterprise/aiproxyd/aiproxyd_test.go +++ b/enterprise/aibridgeproxyd/aibridgeproxyd_test.go @@ -1,4 +1,4 @@ -package aiproxyd_test +package aibridgeproxyd_test import ( "crypto/rand" @@ -22,7 +22,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/enterprise/aiproxyd" + "github.com/coder/coder/v2/enterprise/aibridgeproxyd" "github.com/coder/coder/v2/testutil" ) @@ -72,7 +72,7 @@ func TestNew(t *testing.T) { certFile, keyFile := generateTestCA(t) logger := slogtest.Make(t, nil) - _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + _, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: "", CertFile: certFile, KeyFile: keyFile, @@ -86,7 +86,7 @@ func TestNew(t *testing.T) { logger := slogtest.Make(t, nil) - _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + _, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: ":0", KeyFile: "key.pem", }) @@ -99,7 +99,7 @@ func TestNew(t *testing.T) { logger := slogtest.Make(t, nil) - _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + _, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: ":0", CertFile: "cert.pem", }) @@ -112,7 +112,7 @@ func TestNew(t *testing.T) { logger := slogtest.Make(t, nil) - _, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + _, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: ":0", CertFile: "/nonexistent/cert.pem", KeyFile: "/nonexistent/key.pem", @@ -127,7 +127,7 @@ func TestNew(t *testing.T) { certFile, keyFile := generateTestCA(t) logger := slogtest.Make(t, nil) - srv, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + srv, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: "127.0.0.1:0", CertFile: certFile, KeyFile: keyFile, @@ -146,7 +146,7 @@ func TestClose(t *testing.T) { certFile, keyFile := generateTestCA(t) logger := slogtest.Make(t, nil) - srv, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + srv, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: "127.0.0.1:0", CertFile: certFile, KeyFile: keyFile, @@ -175,7 +175,7 @@ func TestProxy_MITM(t *testing.T) { logger := slogtest.Make(t, nil) // Start the proxy server. - srv, err := aiproxyd.New(t.Context(), logger, aiproxyd.Options{ + srv, err := aibridgeproxyd.New(t.Context(), logger, aibridgeproxyd.Options{ ListenAddr: "127.0.0.1:8888", CertFile: certFile, KeyFile: keyFile, diff --git a/enterprise/cli/aibridgeproxyd.go b/enterprise/cli/aibridgeproxyd.go new file mode 100644 index 0000000000000..6c2a388a987c2 --- /dev/null +++ b/enterprise/cli/aibridgeproxyd.go @@ -0,0 +1,30 @@ +//go:build !slim + +package cli + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/enterprise/aibridgeproxyd" + "github.com/coder/coder/v2/enterprise/coderd" +) + +func newAIBridgeProxyDaemon(coderAPI *coderd.API) (*aibridgeproxyd.Server, error) { + ctx := context.Background() + coderAPI.Logger.Debug(ctx, "starting in-memory aibridgeproxy daemon") + + logger := coderAPI.Logger.Named("aibridgeproxyd") + + srv, err := aibridgeproxyd.New(ctx, logger, aibridgeproxyd.Options{ + ListenAddr: coderAPI.DeploymentValues.AI.BridgeProxyConfig.ListenAddr.String(), + CertFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.CertFile.String(), + KeyFile: coderAPI.DeploymentValues.AI.BridgeProxyConfig.KeyFile.String(), + }) + if err != nil { + return nil, xerrors.Errorf("failed to start in-memory aibridgeproxy daemon: %w", err) + } + + return srv, nil +} diff --git a/enterprise/cli/aiproxyd.go b/enterprise/cli/aiproxyd.go deleted file mode 100644 index c1411dcf09e43..0000000000000 --- a/enterprise/cli/aiproxyd.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build !slim - -package cli - -import ( - "context" - - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/enterprise/aiproxyd" - "github.com/coder/coder/v2/enterprise/coderd" -) - -func newAIProxyDaemon(coderAPI *coderd.API) (*aiproxyd.Server, error) { - ctx := context.Background() - coderAPI.Logger.Debug(ctx, "starting in-memory aiproxy daemon") - - logger := coderAPI.Logger.Named("aiproxyd") - - srv, err := aiproxyd.New(ctx, logger, aiproxyd.Options{ - ListenAddr: coderAPI.DeploymentValues.AI.ProxyConfig.ListenAddr.String(), - CertFile: coderAPI.DeploymentValues.AI.ProxyConfig.CertFile.String(), - KeyFile: coderAPI.DeploymentValues.AI.ProxyConfig.KeyFile.String(), - }) - if err != nil { - return nil, xerrors.Errorf("failed to start in-memory aiproxy daemon: %w", err) - } - - return srv, nil -} diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index c4686bab52510..5aa7c82a0a505 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -166,11 +166,11 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { } // In-memory AI proxy daemon - if options.DeploymentValues.AI.ProxyConfig.Enabled.Value() { - aiProxyServer, err := newAIProxyDaemon(api) + if options.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() { + aiProxyServer, err := newAIBridgeProxyDaemon(api) if err != nil { _ = closers.Close() - return nil, nil, xerrors.Errorf("create aiproxyd: %w", err) + return nil, nil, xerrors.Errorf("create aibridgeproxyd: %w", err) } closers.Add(aiProxyServer) } diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 7aa7866085d10..7e56328fe8deb 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -140,18 +140,18 @@ AI BRIDGE OPTIONS: Maximum number of AI Bridge requests per second per replica. Set to 0 to disable (unlimited). -AI PROXY OPTIONS: - --aiproxy-cert-file string, $CODER_AIPROXY_CERT_FILE +AI BRIDGE PROXY OPTIONS: + --aibridge-proxy-cert-file string, $CODER_AIBRIDGE_PROXY_CERT_FILE Path to the CA certificate file for MITM. - --aiproxy-enabled bool, $CODER_AIPROXY_ENABLED (default: false) + --aibridge-proxy-enabled bool, $CODER_AIBRIDGE_PROXY_ENABLED (default: false) Enable the AI MITM proxy for intercepting and decrypting AI provider requests. - --aiproxy-key-file string, $CODER_AIPROXY_KEY_FILE + --aibridge-proxy-key-file string, $CODER_AIBRIDGE_PROXY_KEY_FILE Path to the CA private key file for MITM. - --aiproxy-listen-addr string, $CODER_AIPROXY_LISTEN_ADDR (default: :8888) + --aibridge-proxy-listen-addr string, $CODER_AIBRIDGE_PROXY_LISTEN_ADDR (default: :8888) The address the AI proxy will listen on. CLIENT OPTIONS: diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 814e88ae609ca..43d38fc6178cb 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -65,6 +65,14 @@ export interface AIBridgeOpenAIConfig { readonly key: string; } +// From codersdk/deployment.go +export interface AIBridgeProxyConfig { + readonly enabled: boolean; + readonly listen_addr: string; + readonly cert_file: string; + readonly key_file: string; +} + // From codersdk/aibridge.go export interface AIBridgeTokenUsage { readonly id: string; @@ -106,15 +114,7 @@ export interface AIBridgeUserPrompt { // From codersdk/deployment.go export interface AIConfig { readonly bridge?: AIBridgeConfig; - readonly proxy?: AIProxyConfig; -} - -// From codersdk/deployment.go -export interface AIProxyConfig { - readonly enabled: boolean; - readonly listen_addr: string; - readonly cert_file: string; - readonly key_file: string; + readonly aibridge_proxy?: AIBridgeProxyConfig; } // From codersdk/allowlist.go From c90790ff2385f6fa1535bdbf098cc35db61233f9 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 17 Dec 2025 13:35:05 +0000 Subject: [PATCH 4/5] chore: add missing aibridgeproxyd renames --- cli/testdata/coder_server_--help.golden | 10 +++++----- cli/testdata/server-config.yaml.golden | 9 +++++---- codersdk/deployment.go | 8 ++++---- docs/reference/cli/server.md | 8 ++++---- enterprise/aibridgeproxyd/aibridgeproxyd.go | 6 +++--- enterprise/cli/server.go | 6 +++--- enterprise/cli/testdata/coder_server_--help.golden | 10 +++++----- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index bdbd5076e9d6f..13900f7687a79 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -141,17 +141,17 @@ AI BRIDGE OPTIONS: AI BRIDGE PROXY OPTIONS: --aibridge-proxy-cert-file string, $CODER_AIBRIDGE_PROXY_CERT_FILE - Path to the CA certificate file for MITM. + Path to the CA certificate file for AI Bridge Proxy. --aibridge-proxy-enabled bool, $CODER_AIBRIDGE_PROXY_ENABLED (default: false) - Enable the AI MITM proxy for intercepting and decrypting AI provider - requests. + Enable the AI Bridge MITM Proxy for intercepting and decrypting AI + provider requests. --aibridge-proxy-key-file string, $CODER_AIBRIDGE_PROXY_KEY_FILE - Path to the CA private key file for MITM. + Path to the CA private key file for AI Bridge Proxy. --aibridge-proxy-listen-addr string, $CODER_AIBRIDGE_PROXY_LISTEN_ADDR (default: :8888) - The address the AI proxy will listen on. + The address the AI Bridge Proxy will listen on. CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 9181b9cb4c561..63a28ff3b907e 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -766,16 +766,17 @@ aibridge: # (default: 0, type: int) rateLimit: 0 aibridgeproxy: - # Enable the AI MITM proxy for intercepting and decrypting AI provider requests. + # Enable the AI Bridge MITM Proxy for intercepting and decrypting AI provider + # requests. # (default: false, type: bool) enabled: false - # The address the AI proxy will listen on. + # The address the AI Bridge Proxy will listen on. # (default: :8888, type: string) listen_addr: :8888 - # Path to the CA certificate file for MITM. + # Path to the CA certificate file for AI Bridge Proxy. # (default: , type: string) cert_file: "" - # Path to the CA private key file for MITM. + # Path to the CA private key file for AI Bridge Proxy. # (default: , type: string) key_file: "" # Configure data retention policies for various database tables. Retention diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 1d9f2f04704ed..218cf94eca8b4 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3451,7 +3451,7 @@ Write out the current server config as YAML to stdout.`, // AI Bridge Proxy Options { Name: "AI Bridge Proxy Enabled", - Description: "Enable the AI MITM proxy for intercepting and decrypting AI provider requests.", + Description: "Enable the AI Bridge MITM Proxy for intercepting and decrypting AI provider requests.", Flag: "aibridge-proxy-enabled", Env: "CODER_AIBRIDGE_PROXY_ENABLED", Value: &c.AI.BridgeProxyConfig.Enabled, @@ -3461,7 +3461,7 @@ Write out the current server config as YAML to stdout.`, }, { Name: "AI Bridge Proxy Listen Address", - Description: "The address the AI proxy will listen on.", + Description: "The address the AI Bridge Proxy will listen on.", Flag: "aibridge-proxy-listen-addr", Env: "CODER_AIBRIDGE_PROXY_LISTEN_ADDR", Value: &c.AI.BridgeProxyConfig.ListenAddr, @@ -3471,7 +3471,7 @@ Write out the current server config as YAML to stdout.`, }, { Name: "AI Bridge Proxy Certificate File", - Description: "Path to the CA certificate file for MITM.", + Description: "Path to the CA certificate file for AI Bridge Proxy.", Flag: "aibridge-proxy-cert-file", Env: "CODER_AIBRIDGE_PROXY_CERT_FILE", Value: &c.AI.BridgeProxyConfig.CertFile, @@ -3481,7 +3481,7 @@ Write out the current server config as YAML to stdout.`, }, { Name: "AI Bridge Proxy Key File", - Description: "Path to the CA private key file for MITM.", + Description: "Path to the CA private key file for AI Bridge Proxy.", Flag: "aibridge-proxy-key-file", Env: "CODER_AIBRIDGE_PROXY_KEY_FILE", Value: &c.AI.BridgeProxyConfig.KeyFile, diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 6a9d718295658..d3ea11879d0f0 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1823,7 +1823,7 @@ Maximum number of AI Bridge requests per second per replica. Set to 0 to disable | YAML | aibridgeproxy.enabled | | Default | false | -Enable the AI MITM proxy for intercepting and decrypting AI provider requests. +Enable the AI Bridge MITM Proxy for intercepting and decrypting AI provider requests. ### --aibridge-proxy-listen-addr @@ -1834,7 +1834,7 @@ Enable the AI MITM proxy for intercepting and decrypting AI provider requests. | YAML | aibridgeproxy.listen_addr | | Default | :8888 | -The address the AI proxy will listen on. +The address the AI Bridge Proxy will listen on. ### --aibridge-proxy-cert-file @@ -1844,7 +1844,7 @@ The address the AI proxy will listen on. | Environment | $CODER_AIBRIDGE_PROXY_CERT_FILE | | YAML | aibridgeproxy.cert_file | -Path to the CA certificate file for MITM. +Path to the CA certificate file for AI Bridge Proxy. ### --aibridge-proxy-key-file @@ -1854,7 +1854,7 @@ Path to the CA certificate file for MITM. | Environment | $CODER_AIBRIDGE_PROXY_KEY_FILE | | YAML | aibridgeproxy.key_file | -Path to the CA private key file for MITM. +Path to the CA private key file for AI Bridge Proxy. ### --audit-logs-retention diff --git a/enterprise/aibridgeproxyd/aibridgeproxyd.go b/enterprise/aibridgeproxyd/aibridgeproxyd.go index 8db4bcc4e6855..0d3169c7cdd19 100644 --- a/enterprise/aibridgeproxyd/aibridgeproxyd.go +++ b/enterprise/aibridgeproxyd/aibridgeproxyd.go @@ -25,7 +25,7 @@ type Server struct { httpServer *http.Server } -// Options configures the AI proxy server. +// Options configures the AI Bridge Proxy server. type Options struct { // ListenAddr is the address the proxy server will listen on. ListenAddr string @@ -36,7 +36,7 @@ type Options struct { } func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error) { - logger.Info(ctx, "initializing AI proxy server") + logger.Info(ctx, "initializing AI Bridge Proxy server") if opts.ListenAddr == "" { return nil, xerrors.New("listen address is required") @@ -72,7 +72,7 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error) } go func() { - logger.Info(ctx, "starting AI proxy", slog.F("addr", opts.ListenAddr)) + logger.Info(ctx, "starting AI Bridge Proxy", slog.F("addr", opts.ListenAddr)) if err := srv.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error(ctx, "aibridgeproxyd server error", slog.Error(err)) } diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 5aa7c82a0a505..6950144a75624 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -165,14 +165,14 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { closers.Add(aibridgeDaemon) } - // In-memory AI proxy daemon + // In-memory AI Bridge Proxy daemon if options.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() { - aiProxyServer, err := newAIBridgeProxyDaemon(api) + aiBridgeProxyServer, err := newAIBridgeProxyDaemon(api) if err != nil { _ = closers.Close() return nil, nil, xerrors.Errorf("create aibridgeproxyd: %w", err) } - closers.Add(aiProxyServer) + closers.Add(aiBridgeProxyServer) } return api.AGPL, closers, nil diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 7e56328fe8deb..7e09efc337fb7 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -142,17 +142,17 @@ AI BRIDGE OPTIONS: AI BRIDGE PROXY OPTIONS: --aibridge-proxy-cert-file string, $CODER_AIBRIDGE_PROXY_CERT_FILE - Path to the CA certificate file for MITM. + Path to the CA certificate file for AI Bridge Proxy. --aibridge-proxy-enabled bool, $CODER_AIBRIDGE_PROXY_ENABLED (default: false) - Enable the AI MITM proxy for intercepting and decrypting AI provider - requests. + Enable the AI Bridge MITM Proxy for intercepting and decrypting AI + provider requests. --aibridge-proxy-key-file string, $CODER_AIBRIDGE_PROXY_KEY_FILE - Path to the CA private key file for MITM. + Path to the CA private key file for AI Bridge Proxy. --aibridge-proxy-listen-addr string, $CODER_AIBRIDGE_PROXY_LISTEN_ADDR (default: :8888) - The address the AI proxy will listen on. + The address the AI Bridge Proxy will listen on. CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. From db9ee4201c89fc8e26f48b69c0cf5cc81e04029c Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 17 Dec 2025 17:19:03 +0000 Subject: [PATCH 5/5] chore: address comments --- enterprise/aibridgeproxyd/aibridgeproxyd.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/enterprise/aibridgeproxyd/aibridgeproxyd.go b/enterprise/aibridgeproxyd/aibridgeproxyd.go index 0d3169c7cdd19..d70c7699e85db 100644 --- a/enterprise/aibridgeproxyd/aibridgeproxyd.go +++ b/enterprise/aibridgeproxyd/aibridgeproxyd.go @@ -56,7 +56,8 @@ func New(ctx context.Context, logger slog.Logger, opts Options) (*Server, error) // Decrypt all HTTPS requests via MITM. Requests are forwarded to // the original destination without modification for now. // TODO(ssncferreira): Route requests to aibridged - // See https://github.com/coder/internal/issues/1181 + // will be implemented upstack. + // Related to https://github.com/coder/internal/issues/1181 proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) srv := &Server{