mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add core AI MITM proxy daemon
This commit is contained in:
+14
@@ -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.
|
||||
|
||||
+13
@@ -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: <unset>, type: string)
|
||||
cert_file: ""
|
||||
# Path to the CA private key file for MITM.
|
||||
# (default: <unset>, 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
|
||||
|
||||
Generated
+20
@@ -12110,6 +12110,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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+20
@@ -10774,6 +10774,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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Generated
+6
@@ -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,
|
||||
|
||||
Generated
+39
@@ -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
|
||||
|
||||
@@ -2616,6 +2643,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,
|
||||
@@ -3147,6 +3180,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,
|
||||
|
||||
Generated
+42
@@ -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 | <code>bool</code> |
|
||||
| Environment | <code>$CODER_AIPROXY_ENABLED</code> |
|
||||
| YAML | <code>aiproxy.enabled</code> |
|
||||
| Default | <code>false</code> |
|
||||
|
||||
Enable the AI MITM proxy for intercepting and decrypting AI provider requests.
|
||||
|
||||
### --aiproxy-listen-addr
|
||||
|
||||
| | |
|
||||
|-------------|-----------------------------------------|
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AIPROXY_LISTEN_ADDR</code> |
|
||||
| YAML | <code>aiproxy.listen_addr</code> |
|
||||
| Default | <code>:8888</code> |
|
||||
|
||||
The address the AI proxy will listen on.
|
||||
|
||||
### --aiproxy-cert-file
|
||||
|
||||
| | |
|
||||
|-------------|---------------------------------------|
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AIPROXY_CERT_FILE</code> |
|
||||
| YAML | <code>aiproxy.cert_file</code> |
|
||||
|
||||
Path to the CA certificate file for MITM.
|
||||
|
||||
### --aiproxy-key-file
|
||||
|
||||
| | |
|
||||
|-------------|--------------------------------------|
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_AIPROXY_KEY_FILE</code> |
|
||||
| YAML | <code>aiproxy.key_file</code> |
|
||||
|
||||
Path to the CA private key file for MITM.
|
||||
|
||||
### --audit-logs-retention
|
||||
|
||||
| | |
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1049,6 +1049,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=
|
||||
|
||||
Generated
+9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user