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:
parent
ac49e6eef5
commit
b6c2998eb3
89
registry/coder/modules/aibridge-proxy/README.md
Normal file
89
registry/coder/modules/aibridge-proxy/README.md
Normal 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"
|
||||
}
|
||||
```
|
||||
254
registry/coder/modules/aibridge-proxy/main.test.ts
Normal file
254
registry/coder/modules/aibridge-proxy/main.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
81
registry/coder/modules/aibridge-proxy/main.tf
Normal file
81
registry/coder/modules/aibridge-proxy/main.tf
Normal 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,
|
||||
})
|
||||
}
|
||||
210
registry/coder/modules/aibridge-proxy/main.tftest.hcl
Normal file
210
registry/coder/modules/aibridge-proxy/main.tftest.hcl
Normal 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"
|
||||
}
|
||||
}
|
||||
79
registry/coder/modules/aibridge-proxy/scripts/setup.sh
Normal file
79
registry/coder/modules/aibridge-proxy/scripts/setup.sh
Normal 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."
|
||||
Loading…
x
Reference in New Issue
Block a user