feat: add aibridge-proxy module for AI Bridge Proxy workspace setup (#721)

## Description

Add `aibridge-proxy` module that configures workspaces to use AI Bridge Proxy. Downloads the proxy's CA certificate and exposes `proxy_auth_url` and `cert_path` outputs for tool-specific modules to configure the proxy scoped to their process. The module does not set proxy environment variables globally in the workspace.

## Type of Change

- [x] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/aibridge-proxy`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

Closes: https://github.com/coder/internal/issues/1187
This commit is contained in:
Susana Ferreira 2026-03-05 09:27:01 +00:00 committed by GitHub
parent ac49e6eef5
commit b6c2998eb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 713 additions and 0 deletions

View File

@ -0,0 +1,89 @@
---
display_name: AI Bridge Proxy
description: Configure a workspace to route AI tool traffic through AI Bridge via AI Bridge Proxy.
icon: ../../../../.icons/coder.svg
verified: true
tags: [helper, aibridge]
---
# AI Bridge Proxy
This module configures a Coder workspace to use [AI Bridge Proxy](https://coder.com/docs/ai-coder/ai-bridge/ai-bridge-proxy).
It downloads the proxy's CA certificate from the Coder deployment and provides Terraform outputs (`proxy_auth_url` and `cert_path`) that tool-specific modules can use to route their traffic through the proxy.
```tf
module "aibridge-proxy" {
source = "registry.coder.com/coder/aibridge-proxy/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
proxy_url = "https://aiproxy.example.com"
}
```
> [!NOTE]
> AI Bridge Proxy is a Premium Coder feature that requires [AI Governance Add-On](https://coder.com/docs/ai-coder/ai-governance).
> See the [AI Bridge Proxy setup guide](https://coder.com/docs/ai-coder/ai-bridge/ai-bridge-proxy/setup) for details on configuring the proxy on your Coder deployment.
## How it works
[AI Bridge Proxy](https://coder.com/docs/ai-coder/ai-bridge/ai-bridge-proxy) is an HTTP proxy that intercepts traffic to AI providers and forwards it through [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge), enabling centralized LLM management, governance, and cost tracking.
Any process with the proxy environment variables set will route **all** its traffic through the proxy.
This module **does not** set proxy environment variables globally on the workspace.
Instead, it provides Terraform outputs (`proxy_auth_url` and `cert_path`) that tool-specific modules consume to configure proxy routing.
See the [Copilot module](https://registry.coder.com/modules/coder-labs/copilot) for a working integration example.
It is recommended that tool modules scope the proxy environment variables to their own process rather than setting them globally on the workspace, to avoid routing unnecessary traffic through the proxy.
> [!WARNING]
> If the setup script fails (e.g. the proxy is unreachable), the workspace will still start but the agent will report a startup script error.
> Tools that depend on the proxy will not work until the issue is resolved. Check the workspace build logs for details.
## Startup Coordination
When used with tool-specific modules (e.g. [Copilot](https://registry.coder.com/modules/coder-labs/copilot)),
the setup script signals completion via [`coder exp sync`](https://coder.com/docs/admin/templates/startup-coordination) so dependent modules can wait for the `aibridge-proxy` module to complete before starting.
Dependent modules are unblocked once the setup script finishes, regardless of success or failure.
If the setup fails, dependent modules are expected to detect the failure and handle the error accordingly.
To enable startup coordination, set `CODER_AGENT_SOCKET_SERVER_ENABLED=true` in the workspace container environment:
```hcl
env = [
"CODER_AGENT_TOKEN=${coder_agent.main.token}",
"CODER_AGENT_SOCKET_SERVER_ENABLED=true",
]
```
> [!NOTE]
> [Startup coordination](https://coder.com/docs/admin/templates/startup-coordination) requires Coder >= v2.30.
> Without it, the sync calls are skipped gracefully but dependent modules may fail to start if the `aibridge-proxy` setup has not completed in time.
## Examples
### Custom certificate path
```tf
module "aibridge-proxy" {
source = "registry.coder.com/coder/aibridge-proxy/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
proxy_url = "https://aiproxy.example.com"
cert_path = "/home/coder/.certs/aibridge-proxy-ca.pem"
}
```
### Proxy with custom port
For deployments where the proxy is accessed directly on a configured port.
See [security considerations](https://coder.com/docs/ai-coder/ai-bridge/ai-bridge-proxy/setup#security-considerations) for network access guidelines.
```tf
module "aibridge-proxy" {
source = "registry.coder.com/coder/aibridge-proxy/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
proxy_url = "http://internal-proxy:8888"
}
```

View File

@ -0,0 +1,254 @@
import { serve } from "bun";
import {
afterEach,
beforeAll,
describe,
expect,
it,
setDefaultTimeout,
} from "bun:test";
import {
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
const FAKE_CERT =
"-----BEGIN CERTIFICATE-----\nMIIBfakecert\n-----END CERTIFICATE-----\n";
// Runs terraform apply to render the setup script, then starts a Docker
// container where we can execute it against a mock server.
const setupContainer = async (vars: Record<string, string> = {}) => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
proxy_url: "https://aiproxy.example.com",
...vars,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("lorello/alpine-bash");
registerCleanup(async () => {
await removeContainer(id);
});
return { id, instance };
};
// Starts a mock HTTP server that simulates the Coder API certificate endpoint.
// Returns the server and its base URL.
const setupServer = (handler: (req: Request) => Response) => {
const server = serve({
fetch: handler,
port: 0,
});
registerCleanup(async () => {
server.stop();
});
return {
server,
// Base URL without trailing slash
url: server.url.toString().slice(0, -1),
};
};
setDefaultTimeout(30 * 1000);
describe("aibridge-proxy", () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
// Verify that agent_id and proxy_url are required.
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
proxy_url: "https://aiproxy.example.com",
});
it("downloads the CA certificate successfully", async () => {
let receivedToken = "";
const { url } = setupServer((req) => {
const reqUrl = new URL(req.url);
if (reqUrl.pathname === "/api/v2/aibridge/proxy/ca-cert.pem") {
receivedToken = req.headers.get("Coder-Session-Token") || "";
return new Response(FAKE_CERT, {
status: 200,
headers: { "Content-Type": "application/x-pem-file" },
});
}
return new Response("not found", { status: 404 });
});
const { id, instance } = await setupContainer();
// Override ACCESS_URL and SESSION_TOKEN at runtime to point at the mock server.
const exec = await execContainer(id, [
"env",
`ACCESS_URL=${url}`,
"SESSION_TOKEN=test-session-token-123",
"bash",
"-c",
instance.script,
]);
expect(exec.exitCode).toBe(0);
expect(exec.stdout).toContain(
"AI Bridge Proxy CA certificate saved to /tmp/aibridge-proxy/ca-cert.pem",
);
// Verify the cert was written to the default path.
const certContent = await execContainer(id, [
"cat",
"/tmp/aibridge-proxy/ca-cert.pem",
]);
expect(certContent.stdout).toContain("BEGIN CERTIFICATE");
// Verify the session token was sent in the request header.
expect(receivedToken).toBe("test-session-token-123");
});
it("fails when the server is unreachable", async () => {
const { id, instance } = await setupContainer();
// Port 9999 has nothing listening, so curl will fail to connect.
const exec = await execContainer(id, [
"env",
"ACCESS_URL=http://localhost:9999",
"SESSION_TOKEN=mock-token",
"bash",
"-c",
instance.script,
]);
expect(exec.exitCode).not.toBe(0);
expect(exec.stdout).toContain(
"AI Bridge Proxy setup failed: could not connect to",
);
});
it("fails when the server returns a non-200 status", async () => {
const { url } = setupServer(() => {
return new Response("not found", { status: 404 });
});
const { id, instance } = await setupContainer();
const exec = await execContainer(id, [
"env",
`ACCESS_URL=${url}`,
"SESSION_TOKEN=mock-token",
"bash",
"-c",
instance.script,
]);
expect(exec.exitCode).not.toBe(0);
expect(exec.stdout).toContain(
"AI Bridge Proxy setup failed: unexpected response",
);
});
it("fails when the server returns an empty response", async () => {
const { url } = setupServer((req) => {
const reqUrl = new URL(req.url);
if (reqUrl.pathname === "/api/v2/aibridge/proxy/ca-cert.pem") {
return new Response("", { status: 200 });
}
return new Response("not found", { status: 404 });
});
const { id, instance } = await setupContainer();
const exec = await execContainer(id, [
"env",
`ACCESS_URL=${url}`,
"SESSION_TOKEN=mock-token",
"bash",
"-c",
instance.script,
]);
expect(exec.exitCode).not.toBe(0);
expect(exec.stdout).toContain(
"AI Bridge Proxy setup failed: downloaded certificate is empty.",
);
});
it("saves the certificate to a custom path", async () => {
const { url } = setupServer((req) => {
const reqUrl = new URL(req.url);
if (reqUrl.pathname === "/api/v2/aibridge/proxy/ca-cert.pem") {
return new Response(FAKE_CERT, {
status: 200,
headers: { "Content-Type": "application/x-pem-file" },
});
}
return new Response("not found", { status: 404 });
});
// Pass a custom cert_path to terraform apply so the script uses it.
const { id, instance } = await setupContainer({
cert_path: "/tmp/custom/certs/proxy-ca.pem",
});
const exec = await execContainer(id, [
"env",
`ACCESS_URL=${url}`,
"SESSION_TOKEN=mock-token",
"bash",
"-c",
instance.script,
]);
expect(exec.exitCode).toBe(0);
expect(exec.stdout).toContain(
"AI Bridge Proxy CA certificate saved to /tmp/custom/certs/proxy-ca.pem",
);
const certContent = await execContainer(id, [
"cat",
"/tmp/custom/certs/proxy-ca.pem",
]);
expect(certContent.stdout).toContain("BEGIN CERTIFICATE");
});
it("does not create global proxy env vars via coder_env", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
proxy_url: "https://aiproxy.example.com",
});
// Proxy env vars should NOT be set globally via coder_env.
// They are intended to be scoped to specific tool processes.
const proxyEnvVarNames = [
"HTTP_PROXY",
"HTTPS_PROXY",
"NODE_EXTRA_CA_CERTS",
"SSL_CERT_FILE",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
];
const proxyEnvVars = state.resources.filter(
(r) =>
r.type === "coder_env" &&
r.instances.some((i) =>
proxyEnvVarNames.includes(i.attributes.name as string),
),
);
expect(proxyEnvVars.length).toBe(0);
});
});

View File

@ -0,0 +1,81 @@
terraform {
required_version = ">= 1.9"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.12"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "proxy_url" {
type = string
description = "The full URL of the AI Bridge Proxy. Include the port if not using standard ports (e.g. https://aiproxy.example.com or http://internal-proxy:8888)."
validation {
condition = can(regex("^https?://", var.proxy_url))
error_message = "proxy_url must start with http:// or https://."
}
}
variable "cert_path" {
type = string
description = "Absolute path where the AI Bridge Proxy CA certificate will be saved."
default = "/tmp/aibridge-proxy/ca-cert.pem"
validation {
condition = startswith(var.cert_path, "/")
error_message = "cert_path must be an absolute path."
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
# Build the proxy URL with Coder authentication embedded.
# AI Bridge Proxy expects the Coder session token as the password
# in basic auth: http://coder:<token>@host:port
proxy_auth_url = replace(
var.proxy_url,
"://",
"://coder:${data.coder_workspace_owner.me.session_token}@"
)
}
# These outputs are intended to be consumed by tool-specific modules,
# to set proxy environment variables scoped to their process, rather than globally.
output "proxy_auth_url" {
description = "The AI Bridge Proxy URL with Coder authentication embedded (http://coder:<token>@host:port)."
value = local.proxy_auth_url
sensitive = true
}
output "cert_path" {
description = "Path to the downloaded AI Bridge Proxy CA certificate."
value = var.cert_path
}
# Downloads the CA certificate from the Coder deployment.
# This runs on workspace start but does not block login, if the script
# fails, the workspace remains usable and the error is visible in the build logs.
# Tools that depend on the proxy will fail until the certificate is available.
resource "coder_script" "aibridge_proxy_setup" {
agent_id = var.agent_id
display_name = "AI Bridge Proxy Setup"
icon = "/icon/coder.svg"
run_on_start = true
start_blocks_login = false
script = templatefile("${path.module}/scripts/setup.sh", {
CERT_PATH = var.cert_path,
ACCESS_URL = data.coder_workspace.me.access_url,
SESSION_TOKEN = data.coder_workspace_owner.me.session_token,
})
}

View File

@ -0,0 +1,210 @@
run "test_aibridge_proxy_basic" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
}
assert {
condition = var.agent_id == "test-agent-id"
error_message = "Agent ID should match the input variable"
}
assert {
condition = var.proxy_url == "https://aiproxy.example.com"
error_message = "Proxy URL should match the input variable"
}
assert {
condition = var.cert_path == "/tmp/aibridge-proxy/ca-cert.pem"
error_message = "cert_path should default to /tmp/aibridge-proxy/ca-cert.pem"
}
}
run "test_aibridge_proxy_empty_url_validation" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = ""
}
expect_failures = [
var.proxy_url,
]
}
run "test_aibridge_proxy_invalid_url_validation" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "aiproxy.example.com"
}
expect_failures = [
var.proxy_url,
]
}
run "test_aibridge_proxy_url_formats" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
}
assert {
condition = can(regex("^https?://", var.proxy_url))
error_message = "Proxy URL should be a valid URL with scheme"
}
}
run "test_aibridge_proxy_https_with_port" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com:8443"
}
assert {
condition = can(regex("^https?://", var.proxy_url))
error_message = "Proxy URL should support HTTPS with custom port"
}
}
run "test_aibridge_proxy_http_with_port" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "http://internal-proxy:8888"
}
assert {
condition = can(regex("^https?://", var.proxy_url))
error_message = "Proxy URL should support HTTP with custom port"
}
}
run "test_aibridge_proxy_empty_cert_path_validation" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
cert_path = ""
}
expect_failures = [
var.cert_path,
]
}
run "test_aibridge_proxy_relative_cert_path_validation" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
cert_path = "relative/path/ca-cert.pem"
}
expect_failures = [
var.cert_path,
]
}
run "test_aibridge_proxy_custom_cert_path" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
cert_path = "/home/coder/.certs/ca-cert.pem"
}
assert {
condition = var.cert_path == "/home/coder/.certs/ca-cert.pem"
error_message = "cert_path should match the input variable"
}
}
run "test_aibridge_proxy_script" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
}
assert {
condition = coder_script.aibridge_proxy_setup.run_on_start == true
error_message = "Script should run on start"
}
assert {
condition = coder_script.aibridge_proxy_setup.start_blocks_login == false
error_message = "Script should not block login"
}
assert {
condition = coder_script.aibridge_proxy_setup.display_name == "AI Bridge Proxy Setup"
error_message = "Script display name should be 'AI Bridge Proxy Setup'"
}
}
run "test_aibridge_proxy_auth_url_https" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "https://aiproxy.example.com"
}
override_data {
target = data.coder_workspace_owner.me
values = {
session_token = "mock-session-token"
}
}
assert {
condition = output.proxy_auth_url == "https://coder:mock-session-token@aiproxy.example.com"
error_message = "proxy_auth_url should contain the mocked session token"
}
assert {
condition = output.cert_path == "/tmp/aibridge-proxy/ca-cert.pem"
error_message = "cert_path output should match the default"
}
}
run "test_aibridge_proxy_auth_url_http_with_port" {
command = plan
variables {
agent_id = "test-agent-id"
proxy_url = "http://internal-proxy:8888"
}
override_data {
target = data.coder_workspace_owner.me
values = {
session_token = "mock-session-token"
}
}
assert {
condition = output.proxy_auth_url == "http://coder:mock-session-token@internal-proxy:8888"
error_message = "proxy_auth_url should preserve the port"
}
assert {
condition = output.cert_path == "/tmp/aibridge-proxy/ca-cert.pem"
error_message = "cert_path output should match the default"
}
}

View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
if [ -z "$CERT_PATH" ]; then
CERT_PATH="${CERT_PATH}"
fi
if [ -z "$ACCESS_URL" ]; then
ACCESS_URL="${ACCESS_URL}"
fi
if [ -z "$SESSION_TOKEN" ]; then
SESSION_TOKEN="${SESSION_TOKEN}"
fi
set -euo pipefail
# Signal startup coordination.
# The trap ensures 'complete' is always called (even on failure) so dependent
# scripts unblock promptly and can check for the certificate themselves.
if command -v coder > /dev/null 2>&1; then
coder exp sync start "aibridge-proxy-setup" > /dev/null 2>&1 || true
trap 'coder exp sync complete "aibridge-proxy-setup" > /dev/null 2>&1 || true' EXIT
fi
if [ -z "$ACCESS_URL" ]; then
echo "Error: Coder access URL is not set."
exit 1
fi
if [ -z "$SESSION_TOKEN" ]; then
echo "Error: Coder session token is not set."
exit 1
fi
if ! command -v curl > /dev/null; then
echo "Error: curl is not installed."
exit 1
fi
echo "--------------------------------"
echo "AI Bridge Proxy Setup"
printf "Certificate path: %s\n" "$CERT_PATH"
printf "Access URL: %s\n" "$ACCESS_URL"
echo "--------------------------------"
CERT_DIR=$(dirname "$CERT_PATH")
mkdir -p "$CERT_DIR"
CERT_URL="$ACCESS_URL/api/v2/aibridge/proxy/ca-cert.pem"
echo "Downloading AI Bridge Proxy CA certificate from $CERT_URL..."
# Download the certificate with a 5s connection timeout and 10s total timeout
# to avoid the script hanging indefinitely.
if ! HTTP_STATUS=$(curl -s -o "$CERT_PATH" -w "%%{http_code}" \
--connect-timeout 5 \
--max-time 10 \
-H "Coder-Session-Token: $SESSION_TOKEN" \
"$CERT_URL"); then
echo "❌ AI Bridge Proxy setup failed: could not connect to $CERT_URL."
echo "Ensure AI Bridge Proxy is enabled and reachable from the workspace."
rm -f "$CERT_PATH"
exit 1
fi
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "❌ AI Bridge Proxy setup failed: unexpected response (HTTP $HTTP_STATUS)."
echo "Ensure AI Bridge Proxy is enabled and reachable from the workspace."
rm -f "$CERT_PATH"
exit 1
fi
if [ ! -s "$CERT_PATH" ]; then
echo "❌ AI Bridge Proxy setup failed: downloaded certificate is empty."
rm -f "$CERT_PATH"
exit 1
fi
echo "AI Bridge Proxy CA certificate saved to $CERT_PATH"
echo "✅ AI Bridge Proxy setup complete."