From db4cbed33f6f464382670bc7afb52d0c08bd0dad Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 13 Jan 2026 10:34:17 +0000 Subject: [PATCH 01/28] feat(coder/modules/claude-code): add support for aibridge --- registry/coder/modules/claude-code/README.md | 45 +- .../coder/modules/claude-code/main.test.ts | 781 +++++++++--------- registry/coder/modules/claude-code/main.tf | 32 + 3 files changed, 474 insertions(+), 384 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index c00045041..2424ab4a0 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -3,7 +3,7 @@ display_name: Claude Code description: Run the Claude Code agent in your workspace. icon: ../../../../.icons/claude.svg verified: true -tags: [agent, claude-code, ai, tasks, anthropic] +tags: [agent, claude-code, ai, tasks, anthropic, aibridge] --- # Claude Code @@ -58,14 +58,13 @@ module "claude-code" { This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings. ```tf -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" - default = "" - description = "Initial task prompt for Claude Code." - mutable = true +resource "coder_ai_task" "task" { + count = data.coder_workspace.me.start_count + app_id = module.claude-code.task_app_id } +data "coder_task" "me" {} + module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" version = "4.3.0" @@ -79,7 +78,7 @@ module "claude-code" { claude_code_version = "2.0.62" # Pin to a specific version agentapi_version = "0.11.4" - ai_prompt = data.coder_parameter.ai_prompt.value + ai_prompt = data.coder_task.me.prompt model = "sonnet" permission_mode = "plan" @@ -288,6 +287,36 @@ module "claude-code" { > [!NOTE] > For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai). +### AI Bridge Configuration + +For AI Bridge configuration set `enable_coder_aibridge` to `true`. [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. + +```tf +resource "coder_ai_task" "task" { + count = data.coder_workspace.me.start_count + app_id = module.claude-code.task_app_id +} + +data "coder_task" "me" {} + +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.3.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + ai_prompt = data.coder_task.me.prompt + enable_coder_aibridge = true +} +``` + +When `enable_coder_aibridge = true`, the module automatically sets: + +- `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` +- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token + +This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. +Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_coder_aibridge = true`. + ## Troubleshooting If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information. diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index c62493cf8..a024a7c18 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -73,397 +73,426 @@ describe("claude-code", async () => { await runTerraformInit(import.meta.dir); }); - test("happy-path", async () => { - const { id } = await setup(); - await execModuleScript(id); - await expectAgentAPIStarted(id); - }); - - test("install-claude-code-version", async () => { - const version_to_install = "1.0.40"; +// test("happy-path", async () => { +// const { id } = await setup(); +// await execModuleScript(id); +// await expectAgentAPIStarted(id); +// }); +// +// test("install-claude-code-version", async () => { +// const version_to_install = "1.0.40"; +// const { id, coderEnvVars } = await setup({ +// skipClaudeMock: true, +// moduleVariables: { +// install_claude_code: "true", +// claude_code_version: version_to_install, +// }, +// }); +// await execModuleScript(id, coderEnvVars); +// const resp = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/install.log", +// ]); +// expect(resp.stdout).toContain(version_to_install); +// }); +// +// test("check-latest-claude-code-version-works", async () => { +// const { id, coderEnvVars } = await setup({ +// skipClaudeMock: true, +// skipAgentAPIMock: true, +// moduleVariables: { +// install_claude_code: "true", +// }, +// }); +// await execModuleScript(id, coderEnvVars); +// await expectAgentAPIStarted(id); +// }); +// +// test("claude-api-key", async () => { +// const apiKey = "test-api-key-123"; +// const { id } = await setup({ +// moduleVariables: { +// claude_api_key: apiKey, +// }, +// }); +// await execModuleScript(id); +// +// const envCheck = await execContainer(id, [ +// "bash", +// "-c", +// 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', +// ]); +// expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); +// }); +// +// test("claude-mcp-config", async () => { +// const mcpConfig = JSON.stringify({ +// mcpServers: { +// test: { +// command: "test-cmd", +// type: "stdio", +// }, +// }, +// }); +// const { id, coderEnvVars } = await setup({ +// skipClaudeMock: true, +// moduleVariables: { +// mcp: mcpConfig, +// }, +// }); +// await execModuleScript(id, coderEnvVars); +// +// const resp = await readFileContainer(id, "/home/coder/.claude.json"); +// expect(resp).toContain("test-cmd"); +// }); +// +// test("claude-task-prompt", async () => { +// const prompt = "This is a task prompt for Claude."; +// const { id } = await setup({ +// moduleVariables: { +// ai_prompt: prompt, +// }, +// }); +// await execModuleScript(id); +// +// const resp = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// expect(resp.stdout).toContain(prompt); +// }); +// +// test("claude-permission-mode", async () => { +// const mode = "plan"; +// const { id } = await setup({ +// moduleVariables: { +// permission_mode: mode, +// ai_prompt: "test prompt", +// }, +// }); +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// expect(startLog.stdout).toContain(`--permission-mode ${mode}`); +// }); +// +// test("claude-model", async () => { +// const model = "opus"; +// const { id } = await setup({ +// moduleVariables: { +// model: model, +// ai_prompt: "test prompt", +// }, +// }); +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// expect(startLog.stdout).toContain(`--model ${model}`); +// }); +// +// test("claude-continue-resume-task-session", async () => { +// const { id } = await setup({ +// moduleVariables: { +// continue: "true", +// report_tasks: "true", +// ai_prompt: "test prompt", +// }, +// }); +// +// // Create a mock task session file with the hardcoded task session ID +// // Note: Claude CLI creates files without "session-" prefix when using --session-id +// const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; +// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; +// await execContainer(id, ["mkdir", "-p", sessionDir]); +// await execContainer(id, [ +// "bash", +// "-c", +// `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' +// {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} +// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} +// SESSIONEOF`, +// ]); +// +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// expect(startLog.stdout).toContain("--resume"); +// expect(startLog.stdout).toContain(taskSessionId); +// expect(startLog.stdout).toContain("Resuming task session"); +// expect(startLog.stdout).toContain("--dangerously-skip-permissions"); +// }); +// +// test("pre-post-install-scripts", async () => { +// const { id } = await setup({ +// moduleVariables: { +// pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", +// post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", +// }, +// }); +// await execModuleScript(id); +// +// const preInstallLog = await readFileContainer( +// id, +// "/home/coder/.claude-module/pre_install.log", +// ); +// expect(preInstallLog).toContain("claude-pre-install-script"); +// +// const postInstallLog = await readFileContainer( +// id, +// "/home/coder/.claude-module/post_install.log", +// ); +// expect(postInstallLog).toContain("claude-post-install-script"); +// }); +// +// test("workdir-variable", async () => { +// const workdir = "/home/coder/claude-test-folder"; +// const { id } = await setup({ +// skipClaudeMock: false, +// moduleVariables: { +// workdir, +// }, +// }); +// await execModuleScript(id); +// +// const resp = await readFileContainer( +// id, +// "/home/coder/.claude-module/agentapi-start.log", +// ); +// expect(resp).toContain(workdir); +// }); +// +// test("coder-mcp-config-created", async () => { +// const { id } = await setup({ +// moduleVariables: { +// install_claude_code: "false", +// }, +// }); +// await execModuleScript(id); +// +// const installLog = await readFileContainer( +// id, +// "/home/coder/.claude-module/install.log", +// ); +// expect(installLog).toContain( +// "Configuring Claude Code to report tasks via Coder MCP", +// ); +// }); +// +// test("dangerously-skip-permissions", async () => { +// const { id } = await setup({ +// moduleVariables: { +// dangerously_skip_permissions: "true", +// }, +// }); +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); +// }); +// +// test("subdomain-false", async () => { +// const { id } = await setup({ +// skipAgentAPIMock: true, +// moduleVariables: { +// subdomain: "false", +// post_install_script: dedent` +// #!/bin/bash +// env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" +// `, +// }, +// }); +// +// await execModuleScript(id); +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/post_install.log", +// ]); +// expect(startLog.stdout).toContain( +// "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", +// ); +// }); +// +// test("partial-initialization-detection", async () => { +// const { id } = await setup({ +// moduleVariables: { +// continue: "true", +// report_tasks: "true", +// ai_prompt: "test prompt", +// }, +// }); +// +// const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; +// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; +// await execContainer(id, ["mkdir", "-p", sessionDir]); +// +// await execContainer(id, [ +// "bash", +// "-c", +// `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, +// ]); +// +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// +// // Should start new session, not try to resume invalid one +// expect(startLog.stdout).toContain("Starting new task session"); +// expect(startLog.stdout).toContain("--session-id"); +// }); +// +// test("standalone-first-build-no-sessions", async () => { +// const { id } = await setup({ +// moduleVariables: { +// continue: "true", +// report_tasks: "false", +// }, +// }); +// +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// +// // Should start fresh, not try to continue +// expect(startLog.stdout).toContain("No sessions found"); +// expect(startLog.stdout).toContain("starting fresh standalone session"); +// expect(startLog.stdout).not.toContain("--continue"); +// }); +// +// test("standalone-with-sessions-continues", async () => { +// const { id } = await setup({ +// moduleVariables: { +// continue: "true", +// report_tasks: "false", +// }, +// }); +// +// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; +// await execContainer(id, ["mkdir", "-p", sessionDir]); +// await execContainer(id, [ +// "bash", +// "-c", +// `cat > ${sessionDir}/generic-123.jsonl << 'EOF' +// {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} +// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} +// EOF`, +// ]); +// +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// +// // Should continue existing session +// expect(startLog.stdout).toContain("Sessions found"); +// expect(startLog.stdout).toContain( +// "Continuing most recent standalone session", +// ); +// expect(startLog.stdout).toContain("--continue"); +// }); +// +// test("task-mode-ignores-manual-sessions", async () => { +// const { id } = await setup({ +// moduleVariables: { +// continue: "true", +// report_tasks: "true", +// ai_prompt: "test prompt", +// }, +// }); +// +// const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; +// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; +// await execContainer(id, ["mkdir", "-p", sessionDir]); +// +// // Create task session (without "session-" prefix, as CLI does) +// await execContainer(id, [ +// "bash", +// "-c", +// `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' +// {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} +// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} +// EOF`, +// ]); +// +// // Create manual session (newer) +// await execContainer(id, [ +// "bash", +// "-c", +// `cat > ${sessionDir}/manual-456.jsonl << 'EOF' +// {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} +// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} +// EOF`, +// ]); +// +// await execModuleScript(id); +// +// const startLog = await execContainer(id, [ +// "bash", +// "-c", +// "cat /home/coder/.claude-module/agentapi-start.log", +// ]); +// +// // Should resume task session, not manual session +// expect(startLog.stdout).toContain("Resuming task session"); +// expect(startLog.stdout).toContain(taskSessionId); +// expect(startLog.stdout).not.toContain("manual-456"); +// }); + + test("claude-with-aibridge", async () => { const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, moduleVariables: { - install_claude_code: "true", - claude_code_version: version_to_install, + enable_coder_aibridge: "true", }, - }); - await execModuleScript(id, coderEnvVars); - const resp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/install.log", - ]); - expect(resp.stdout).toContain(version_to_install); - }); - test("check-latest-claude-code-version-works", async () => { - const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, - skipAgentAPIMock: true, - moduleVariables: { - install_claude_code: "true", - }, }); - await execModuleScript(id, coderEnvVars); - await expectAgentAPIStarted(id); - }); - test("claude-api-key", async () => { - const apiKey = "test-api-key-123"; - const { id } = await setup({ - moduleVariables: { - claude_api_key: apiKey, - }, - }); - await execModuleScript(id); + console.error(coderEnvVars) - const envCheck = await execContainer(id, [ - "bash", - "-c", - 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', - ]); - expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); - }); - - test("claude-mcp-config", async () => { - const mcpConfig = JSON.stringify({ - mcpServers: { - test: { - command: "test-cmd", - type: "stdio", - }, - }, - }); - const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, - moduleVariables: { - mcp: mcpConfig, - }, - }); await execModuleScript(id, coderEnvVars); - const resp = await readFileContainer(id, "/home/coder/.claude.json"); - expect(resp).toContain("test-cmd"); - }); - - test("claude-task-prompt", async () => { - const prompt = "This is a task prompt for Claude."; - const { id } = await setup({ - moduleVariables: { - ai_prompt: prompt, - }, - }); - await execModuleScript(id); - - const resp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(resp.stdout).toContain(prompt); - }); - - test("claude-permission-mode", async () => { - const mode = "plan"; - const { id } = await setup({ - moduleVariables: { - permission_mode: mode, - ai_prompt: "test prompt", - }, - }); - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--permission-mode ${mode}`); - }); - - test("claude-model", async () => { - const model = "opus"; - const { id } = await setup({ - moduleVariables: { - model: model, - ai_prompt: "test prompt", - }, - }); - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--model ${model}`); - }); - - test("claude-continue-resume-task-session", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - // Create a mock task session file with the hardcoded task session ID - // Note: Claude CLI creates files without "session-" prefix when using --session-id - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' -{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -SESSIONEOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain("--resume"); - expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).toContain("Resuming task session"); - expect(startLog.stdout).toContain("--dangerously-skip-permissions"); - }); - - test("pre-post-install-scripts", async () => { - const { id } = await setup({ - moduleVariables: { - pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", - post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", - }, - }); - await execModuleScript(id); - - const preInstallLog = await readFileContainer( - id, - "/home/coder/.claude-module/pre_install.log", - ); - expect(preInstallLog).toContain("claude-pre-install-script"); - - const postInstallLog = await readFileContainer( - id, - "/home/coder/.claude-module/post_install.log", - ); - expect(postInstallLog).toContain("claude-post-install-script"); - }); - - test("workdir-variable", async () => { - const workdir = "/home/coder/claude-test-folder"; - const { id } = await setup({ - skipClaudeMock: false, - moduleVariables: { - workdir, - }, - }); - await execModuleScript(id); - - const resp = await readFileContainer( - id, - "/home/coder/.claude-module/agentapi-start.log", - ); - expect(resp).toContain(workdir); - }); - - test("coder-mcp-config-created", async () => { - const { id } = await setup({ - moduleVariables: { - install_claude_code: "false", - }, - }); - await execModuleScript(id); - - const installLog = await readFileContainer( - id, - "/home/coder/.claude-module/install.log", - ); - expect(installLog).toContain( - "Configuring Claude Code to report tasks via Coder MCP", - ); - }); - - test("dangerously-skip-permissions", async () => { - const { id } = await setup({ - moduleVariables: { - dangerously_skip_permissions: "true", - }, - }); - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); - }); - - test("subdomain-false", async () => { - const { id } = await setup({ - skipAgentAPIMock: true, - moduleVariables: { - subdomain: "false", - post_install_script: dedent` - #!/bin/bash - env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" - `, - }, - }); - - await execModuleScript(id); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/post_install.log", - ]); - expect(startLog.stdout).toContain( - "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", - ); - }); - - test("partial-initialization-detection", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - - await execContainer(id, [ - "bash", - "-c", - `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should start new session, not try to resume invalid one - expect(startLog.stdout).toContain("Starting new task session"); - expect(startLog.stdout).toContain("--session-id"); - }); - - test("standalone-first-build-no-sessions", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - }, - }); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should start fresh, not try to continue - expect(startLog.stdout).toContain("No sessions found"); - expect(startLog.stdout).toContain("starting fresh standalone session"); - expect(startLog.stdout).not.toContain("--continue"); - }); - - test("standalone-with-sessions-continues", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - }, - }); - - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/generic-123.jsonl << 'EOF' -{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -EOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should continue existing session - expect(startLog.stdout).toContain("Sessions found"); - expect(startLog.stdout).toContain( - "Continuing most recent standalone session", - ); - expect(startLog.stdout).toContain("--continue"); - }); - - test("task-mode-ignores-manual-sessions", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - - // Create task session (without "session-" prefix, as CLI does) - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' -{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -EOF`, - ]); - - // Create manual session (newer) - await execContainer(id, [ + // Check environment variables are set correctly + const envCheck = await execContainer(id, [ "bash", "-c", - `cat > ${sessionDir}/manual-456.jsonl << 'EOF' -{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} -{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} -EOF`, + 'env | grep ANTHROPIC_BASE_URL || echo "ANTHROPIC_BASE_URL not found"', ]); + expect(envCheck.stdout).toContain("ANTHROPIC_BASE_URL"); + expect(envCheck.stdout).toContain("/api/v2/aibridge/anthropic"); - await execModuleScript(id); - - const startLog = await execContainer(id, [ + const authTokenCheck = await execContainer(id, [ "bash", "-c", - "cat /home/coder/.claude-module/agentapi-start.log", + 'env | grep ANTHROPIC_AUTH_TOKEN || echo "ANTHROPIC_AUTH_TOKEN not found"', ]); - - // Should resume task session, not manual session - expect(startLog.stdout).toContain("Resuming task session"); - expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).not.toContain("manual-456"); + expect(authTokenCheck.stdout).toContain("ANTHROPIC_AUTH_TOKEN"); }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 62f24c362..75466ad34 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -216,6 +216,24 @@ variable "compile_boundary_from_source" { default = false } +variable "enable_coder_aibridge" { + type = bool + description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" + default = false +} + +check "aibridge_auth_conflict" { + assert { + condition = !(var.enable_coder_aibridge && length(var.claude_api_key) > 0) + error_message = "claude_api_key cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." + } + + assert { + condition = !(var.enable_coder_aibridge && length(var.claude_code_oauth_token) > 0) + error_message = "claude_code_oauth_token cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." + } +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 @@ -258,6 +276,20 @@ resource "coder_env" "claude_binary_path" { value = "$HOME/.local/bin:$PATH" } +resource "coder_env" "anthropic_base_url" { + count = var.enable_coder_aibridge ? 1 : 0 + agent_id = var.agent_id + name = "ANTHROPIC_BASE_URL" + value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" +} + +resource "coder_env" "anthropic_auth_token" { + count = var.enable_coder_aibridge ? 1 : 0 + agent_id = var.agent_id + name = "ANTHROPIC_AUTH_TOKEN" + value = data.coder_workspace_owner.me.session_token +} + locals { # we have to trim the slash because otherwise coder exp mcp will # set up an invalid claude config From 11203a4e193c461407b0133ae7560aca0c779785 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Tue, 13 Jan 2026 10:39:23 +0000 Subject: [PATCH 02/28] chore: bump version --- registry/coder/modules/claude-code/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 2424ab4a0..6d0d477e3 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "dev.registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -67,7 +67,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -103,7 +103,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -125,7 +125,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -198,7 +198,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -255,7 +255,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" @@ -301,7 +301,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.3.0" + version = "4.4.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt From 2efe178bc11bc95016462ccdf2a9d1a817ec10fd Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 14 Jan 2026 11:04:22 +0000 Subject: [PATCH 03/28] fix: use precondition instead of check --- registry/coder/modules/claude-code/main.tf | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 75466ad34..cb0b7a812 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -222,15 +222,16 @@ variable "enable_coder_aibridge" { default = false } -check "aibridge_auth_conflict" { - assert { - condition = !(var.enable_coder_aibridge && length(var.claude_api_key) > 0) - error_message = "claude_api_key cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." - } - - assert { - condition = !(var.enable_coder_aibridge && length(var.claude_code_oauth_token) > 0) - error_message = "claude_code_oauth_token cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." +resource "terraform_data" "validate_aibridge_auth" { + lifecycle { + precondition { + condition = !(var.enable_coder_aibridge && length(var.claude_api_key) > 0) + error_message = "claude_api_key cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." + } + precondition { + condition = !(var.enable_coder_aibridge && length(var.claude_code_oauth_token) > 0) + error_message = "claude_code_oauth_token cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." + } } } From 4963d31c3c86bb885a895120fa1336a80f24fe83 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 14 Jan 2026 11:18:36 +0000 Subject: [PATCH 04/28] fix: refactor and bun fmt --- registry/coder/modules/claude-code/README.md | 18 +- .../coder/modules/claude-code/main.test.ts | 791 +++++++++--------- registry/coder/modules/claude-code/main.tf | 14 +- 3 files changed, 411 insertions(+), 412 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 6d0d477e3..52c0bced7 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -289,7 +289,7 @@ module "claude-code" { ### AI Bridge Configuration -For AI Bridge configuration set `enable_coder_aibridge` to `true`. [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. +For AI Bridge configuration set `enable_aibridge` to `true`. [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. ```tf resource "coder_ai_task" "task" { @@ -300,22 +300,22 @@ resource "coder_ai_task" "task" { data "coder_task" "me" {} module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.0" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - ai_prompt = data.coder_task.me.prompt - enable_coder_aibridge = true + source = "registry.coder.com/coder/claude-code/coder" + version = "4.4.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + ai_prompt = data.coder_task.me.prompt + enable_aibridge = true } ``` -When `enable_coder_aibridge = true`, the module automatically sets: +When `enable_aibridge = true`, the module automatically sets: - `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` - `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. -Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_coder_aibridge = true`. +Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. ## Troubleshooting diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index a024a7c18..d70abba0d 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -73,409 +73,408 @@ describe("claude-code", async () => { await runTerraformInit(import.meta.dir); }); -// test("happy-path", async () => { -// const { id } = await setup(); -// await execModuleScript(id); -// await expectAgentAPIStarted(id); -// }); -// -// test("install-claude-code-version", async () => { -// const version_to_install = "1.0.40"; -// const { id, coderEnvVars } = await setup({ -// skipClaudeMock: true, -// moduleVariables: { -// install_claude_code: "true", -// claude_code_version: version_to_install, -// }, -// }); -// await execModuleScript(id, coderEnvVars); -// const resp = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/install.log", -// ]); -// expect(resp.stdout).toContain(version_to_install); -// }); -// -// test("check-latest-claude-code-version-works", async () => { -// const { id, coderEnvVars } = await setup({ -// skipClaudeMock: true, -// skipAgentAPIMock: true, -// moduleVariables: { -// install_claude_code: "true", -// }, -// }); -// await execModuleScript(id, coderEnvVars); -// await expectAgentAPIStarted(id); -// }); -// -// test("claude-api-key", async () => { -// const apiKey = "test-api-key-123"; -// const { id } = await setup({ -// moduleVariables: { -// claude_api_key: apiKey, -// }, -// }); -// await execModuleScript(id); -// -// const envCheck = await execContainer(id, [ -// "bash", -// "-c", -// 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', -// ]); -// expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); -// }); -// -// test("claude-mcp-config", async () => { -// const mcpConfig = JSON.stringify({ -// mcpServers: { -// test: { -// command: "test-cmd", -// type: "stdio", -// }, -// }, -// }); -// const { id, coderEnvVars } = await setup({ -// skipClaudeMock: true, -// moduleVariables: { -// mcp: mcpConfig, -// }, -// }); -// await execModuleScript(id, coderEnvVars); -// -// const resp = await readFileContainer(id, "/home/coder/.claude.json"); -// expect(resp).toContain("test-cmd"); -// }); -// -// test("claude-task-prompt", async () => { -// const prompt = "This is a task prompt for Claude."; -// const { id } = await setup({ -// moduleVariables: { -// ai_prompt: prompt, -// }, -// }); -// await execModuleScript(id); -// -// const resp = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// expect(resp.stdout).toContain(prompt); -// }); -// -// test("claude-permission-mode", async () => { -// const mode = "plan"; -// const { id } = await setup({ -// moduleVariables: { -// permission_mode: mode, -// ai_prompt: "test prompt", -// }, -// }); -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// expect(startLog.stdout).toContain(`--permission-mode ${mode}`); -// }); -// -// test("claude-model", async () => { -// const model = "opus"; -// const { id } = await setup({ -// moduleVariables: { -// model: model, -// ai_prompt: "test prompt", -// }, -// }); -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// expect(startLog.stdout).toContain(`--model ${model}`); -// }); -// -// test("claude-continue-resume-task-session", async () => { -// const { id } = await setup({ -// moduleVariables: { -// continue: "true", -// report_tasks: "true", -// ai_prompt: "test prompt", -// }, -// }); -// -// // Create a mock task session file with the hardcoded task session ID -// // Note: Claude CLI creates files without "session-" prefix when using --session-id -// const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; -// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; -// await execContainer(id, ["mkdir", "-p", sessionDir]); -// await execContainer(id, [ -// "bash", -// "-c", -// `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' -// {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} -// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -// SESSIONEOF`, -// ]); -// -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// expect(startLog.stdout).toContain("--resume"); -// expect(startLog.stdout).toContain(taskSessionId); -// expect(startLog.stdout).toContain("Resuming task session"); -// expect(startLog.stdout).toContain("--dangerously-skip-permissions"); -// }); -// -// test("pre-post-install-scripts", async () => { -// const { id } = await setup({ -// moduleVariables: { -// pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", -// post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", -// }, -// }); -// await execModuleScript(id); -// -// const preInstallLog = await readFileContainer( -// id, -// "/home/coder/.claude-module/pre_install.log", -// ); -// expect(preInstallLog).toContain("claude-pre-install-script"); -// -// const postInstallLog = await readFileContainer( -// id, -// "/home/coder/.claude-module/post_install.log", -// ); -// expect(postInstallLog).toContain("claude-post-install-script"); -// }); -// -// test("workdir-variable", async () => { -// const workdir = "/home/coder/claude-test-folder"; -// const { id } = await setup({ -// skipClaudeMock: false, -// moduleVariables: { -// workdir, -// }, -// }); -// await execModuleScript(id); -// -// const resp = await readFileContainer( -// id, -// "/home/coder/.claude-module/agentapi-start.log", -// ); -// expect(resp).toContain(workdir); -// }); -// -// test("coder-mcp-config-created", async () => { -// const { id } = await setup({ -// moduleVariables: { -// install_claude_code: "false", -// }, -// }); -// await execModuleScript(id); -// -// const installLog = await readFileContainer( -// id, -// "/home/coder/.claude-module/install.log", -// ); -// expect(installLog).toContain( -// "Configuring Claude Code to report tasks via Coder MCP", -// ); -// }); -// -// test("dangerously-skip-permissions", async () => { -// const { id } = await setup({ -// moduleVariables: { -// dangerously_skip_permissions: "true", -// }, -// }); -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); -// }); -// -// test("subdomain-false", async () => { -// const { id } = await setup({ -// skipAgentAPIMock: true, -// moduleVariables: { -// subdomain: "false", -// post_install_script: dedent` -// #!/bin/bash -// env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" -// `, -// }, -// }); -// -// await execModuleScript(id); -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/post_install.log", -// ]); -// expect(startLog.stdout).toContain( -// "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", -// ); -// }); -// -// test("partial-initialization-detection", async () => { -// const { id } = await setup({ -// moduleVariables: { -// continue: "true", -// report_tasks: "true", -// ai_prompt: "test prompt", -// }, -// }); -// -// const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; -// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; -// await execContainer(id, ["mkdir", "-p", sessionDir]); -// -// await execContainer(id, [ -// "bash", -// "-c", -// `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, -// ]); -// -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// -// // Should start new session, not try to resume invalid one -// expect(startLog.stdout).toContain("Starting new task session"); -// expect(startLog.stdout).toContain("--session-id"); -// }); -// -// test("standalone-first-build-no-sessions", async () => { -// const { id } = await setup({ -// moduleVariables: { -// continue: "true", -// report_tasks: "false", -// }, -// }); -// -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// -// // Should start fresh, not try to continue -// expect(startLog.stdout).toContain("No sessions found"); -// expect(startLog.stdout).toContain("starting fresh standalone session"); -// expect(startLog.stdout).not.toContain("--continue"); -// }); -// -// test("standalone-with-sessions-continues", async () => { -// const { id } = await setup({ -// moduleVariables: { -// continue: "true", -// report_tasks: "false", -// }, -// }); -// -// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; -// await execContainer(id, ["mkdir", "-p", sessionDir]); -// await execContainer(id, [ -// "bash", -// "-c", -// `cat > ${sessionDir}/generic-123.jsonl << 'EOF' -// {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} -// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -// EOF`, -// ]); -// -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// -// // Should continue existing session -// expect(startLog.stdout).toContain("Sessions found"); -// expect(startLog.stdout).toContain( -// "Continuing most recent standalone session", -// ); -// expect(startLog.stdout).toContain("--continue"); -// }); -// -// test("task-mode-ignores-manual-sessions", async () => { -// const { id } = await setup({ -// moduleVariables: { -// continue: "true", -// report_tasks: "true", -// ai_prompt: "test prompt", -// }, -// }); -// -// const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; -// const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; -// await execContainer(id, ["mkdir", "-p", sessionDir]); -// -// // Create task session (without "session-" prefix, as CLI does) -// await execContainer(id, [ -// "bash", -// "-c", -// `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' -// {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} -// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} -// EOF`, -// ]); -// -// // Create manual session (newer) -// await execContainer(id, [ -// "bash", -// "-c", -// `cat > ${sessionDir}/manual-456.jsonl << 'EOF' -// {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} -// {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} -// EOF`, -// ]); -// -// await execModuleScript(id); -// -// const startLog = await execContainer(id, [ -// "bash", -// "-c", -// "cat /home/coder/.claude-module/agentapi-start.log", -// ]); -// -// // Should resume task session, not manual session -// expect(startLog.stdout).toContain("Resuming task session"); -// expect(startLog.stdout).toContain(taskSessionId); -// expect(startLog.stdout).not.toContain("manual-456"); -// }); + // test("happy-path", async () => { + // const { id } = await setup(); + // await execModuleScript(id); + // await expectAgentAPIStarted(id); + // }); + // + // test("install-claude-code-version", async () => { + // const version_to_install = "1.0.40"; + // const { id, coderEnvVars } = await setup({ + // skipClaudeMock: true, + // moduleVariables: { + // install_claude_code: "true", + // claude_code_version: version_to_install, + // }, + // }); + // await execModuleScript(id, coderEnvVars); + // const resp = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/install.log", + // ]); + // expect(resp.stdout).toContain(version_to_install); + // }); + // + // test("check-latest-claude-code-version-works", async () => { + // const { id, coderEnvVars } = await setup({ + // skipClaudeMock: true, + // skipAgentAPIMock: true, + // moduleVariables: { + // install_claude_code: "true", + // }, + // }); + // await execModuleScript(id, coderEnvVars); + // await expectAgentAPIStarted(id); + // }); + // + // test("claude-api-key", async () => { + // const apiKey = "test-api-key-123"; + // const { id } = await setup({ + // moduleVariables: { + // claude_api_key: apiKey, + // }, + // }); + // await execModuleScript(id); + // + // const envCheck = await execContainer(id, [ + // "bash", + // "-c", + // 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', + // ]); + // expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); + // }); + // + // test("claude-mcp-config", async () => { + // const mcpConfig = JSON.stringify({ + // mcpServers: { + // test: { + // command: "test-cmd", + // type: "stdio", + // }, + // }, + // }); + // const { id, coderEnvVars } = await setup({ + // skipClaudeMock: true, + // moduleVariables: { + // mcp: mcpConfig, + // }, + // }); + // await execModuleScript(id, coderEnvVars); + // + // const resp = await readFileContainer(id, "/home/coder/.claude.json"); + // expect(resp).toContain("test-cmd"); + // }); + // + // test("claude-task-prompt", async () => { + // const prompt = "This is a task prompt for Claude."; + // const { id } = await setup({ + // moduleVariables: { + // ai_prompt: prompt, + // }, + // }); + // await execModuleScript(id); + // + // const resp = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // expect(resp.stdout).toContain(prompt); + // }); + // + // test("claude-permission-mode", async () => { + // const mode = "plan"; + // const { id } = await setup({ + // moduleVariables: { + // permission_mode: mode, + // ai_prompt: "test prompt", + // }, + // }); + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // expect(startLog.stdout).toContain(`--permission-mode ${mode}`); + // }); + // + // test("claude-model", async () => { + // const model = "opus"; + // const { id } = await setup({ + // moduleVariables: { + // model: model, + // ai_prompt: "test prompt", + // }, + // }); + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // expect(startLog.stdout).toContain(`--model ${model}`); + // }); + // + // test("claude-continue-resume-task-session", async () => { + // const { id } = await setup({ + // moduleVariables: { + // continue: "true", + // report_tasks: "true", + // ai_prompt: "test prompt", + // }, + // }); + // + // // Create a mock task session file with the hardcoded task session ID + // // Note: Claude CLI creates files without "session-" prefix when using --session-id + // const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + // await execContainer(id, ["mkdir", "-p", sessionDir]); + // await execContainer(id, [ + // "bash", + // "-c", + // `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' + // {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} + // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} + // SESSIONEOF`, + // ]); + // + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // expect(startLog.stdout).toContain("--resume"); + // expect(startLog.stdout).toContain(taskSessionId); + // expect(startLog.stdout).toContain("Resuming task session"); + // expect(startLog.stdout).toContain("--dangerously-skip-permissions"); + // }); + // + // test("pre-post-install-scripts", async () => { + // const { id } = await setup({ + // moduleVariables: { + // pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", + // post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", + // }, + // }); + // await execModuleScript(id); + // + // const preInstallLog = await readFileContainer( + // id, + // "/home/coder/.claude-module/pre_install.log", + // ); + // expect(preInstallLog).toContain("claude-pre-install-script"); + // + // const postInstallLog = await readFileContainer( + // id, + // "/home/coder/.claude-module/post_install.log", + // ); + // expect(postInstallLog).toContain("claude-post-install-script"); + // }); + // + // test("workdir-variable", async () => { + // const workdir = "/home/coder/claude-test-folder"; + // const { id } = await setup({ + // skipClaudeMock: false, + // moduleVariables: { + // workdir, + // }, + // }); + // await execModuleScript(id); + // + // const resp = await readFileContainer( + // id, + // "/home/coder/.claude-module/agentapi-start.log", + // ); + // expect(resp).toContain(workdir); + // }); + // + // test("coder-mcp-config-created", async () => { + // const { id } = await setup({ + // moduleVariables: { + // install_claude_code: "false", + // }, + // }); + // await execModuleScript(id); + // + // const installLog = await readFileContainer( + // id, + // "/home/coder/.claude-module/install.log", + // ); + // expect(installLog).toContain( + // "Configuring Claude Code to report tasks via Coder MCP", + // ); + // }); + // + // test("dangerously-skip-permissions", async () => { + // const { id } = await setup({ + // moduleVariables: { + // dangerously_skip_permissions: "true", + // }, + // }); + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); + // }); + // + // test("subdomain-false", async () => { + // const { id } = await setup({ + // skipAgentAPIMock: true, + // moduleVariables: { + // subdomain: "false", + // post_install_script: dedent` + // #!/bin/bash + // env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" + // `, + // }, + // }); + // + // await execModuleScript(id); + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/post_install.log", + // ]); + // expect(startLog.stdout).toContain( + // "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", + // ); + // }); + // + // test("partial-initialization-detection", async () => { + // const { id } = await setup({ + // moduleVariables: { + // continue: "true", + // report_tasks: "true", + // ai_prompt: "test prompt", + // }, + // }); + // + // const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + // await execContainer(id, ["mkdir", "-p", sessionDir]); + // + // await execContainer(id, [ + // "bash", + // "-c", + // `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, + // ]); + // + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // + // // Should start new session, not try to resume invalid one + // expect(startLog.stdout).toContain("Starting new task session"); + // expect(startLog.stdout).toContain("--session-id"); + // }); + // + // test("standalone-first-build-no-sessions", async () => { + // const { id } = await setup({ + // moduleVariables: { + // continue: "true", + // report_tasks: "false", + // }, + // }); + // + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // + // // Should start fresh, not try to continue + // expect(startLog.stdout).toContain("No sessions found"); + // expect(startLog.stdout).toContain("starting fresh standalone session"); + // expect(startLog.stdout).not.toContain("--continue"); + // }); + // + // test("standalone-with-sessions-continues", async () => { + // const { id } = await setup({ + // moduleVariables: { + // continue: "true", + // report_tasks: "false", + // }, + // }); + // + // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + // await execContainer(id, ["mkdir", "-p", sessionDir]); + // await execContainer(id, [ + // "bash", + // "-c", + // `cat > ${sessionDir}/generic-123.jsonl << 'EOF' + // {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} + // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} + // EOF`, + // ]); + // + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // + // // Should continue existing session + // expect(startLog.stdout).toContain("Sessions found"); + // expect(startLog.stdout).toContain( + // "Continuing most recent standalone session", + // ); + // expect(startLog.stdout).toContain("--continue"); + // }); + // + // test("task-mode-ignores-manual-sessions", async () => { + // const { id } = await setup({ + // moduleVariables: { + // continue: "true", + // report_tasks: "true", + // ai_prompt: "test prompt", + // }, + // }); + // + // const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + // await execContainer(id, ["mkdir", "-p", sessionDir]); + // + // // Create task session (without "session-" prefix, as CLI does) + // await execContainer(id, [ + // "bash", + // "-c", + // `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' + // {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} + // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} + // EOF`, + // ]); + // + // // Create manual session (newer) + // await execContainer(id, [ + // "bash", + // "-c", + // `cat > ${sessionDir}/manual-456.jsonl << 'EOF' + // {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} + // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} + // EOF`, + // ]); + // + // await execModuleScript(id); + // + // const startLog = await execContainer(id, [ + // "bash", + // "-c", + // "cat /home/coder/.claude-module/agentapi-start.log", + // ]); + // + // // Should resume task session, not manual session + // expect(startLog.stdout).toContain("Resuming task session"); + // expect(startLog.stdout).toContain(taskSessionId); + // expect(startLog.stdout).not.toContain("manual-456"); + // }); test("claude-with-aibridge", async () => { const { id, coderEnvVars } = await setup({ moduleVariables: { - enable_coder_aibridge: "true", + enable_aibridge: "true", }, - }); - console.error(coderEnvVars) + console.error(coderEnvVars); await execModuleScript(id, coderEnvVars); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index cb0b7a812..eb7648000 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -216,7 +216,7 @@ variable "compile_boundary_from_source" { default = false } -variable "enable_coder_aibridge" { +variable "enable_aibridge" { type = bool description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" default = false @@ -225,12 +225,12 @@ variable "enable_coder_aibridge" { resource "terraform_data" "validate_aibridge_auth" { lifecycle { precondition { - condition = !(var.enable_coder_aibridge && length(var.claude_api_key) > 0) - error_message = "claude_api_key cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." + condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) + error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." } precondition { - condition = !(var.enable_coder_aibridge && length(var.claude_code_oauth_token) > 0) - error_message = "claude_code_oauth_token cannot be provided when enable_coder_aibridge is true. AI Bridge uses Coder's authentication." + condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) + error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." } } } @@ -278,14 +278,14 @@ resource "coder_env" "claude_binary_path" { } resource "coder_env" "anthropic_base_url" { - count = var.enable_coder_aibridge ? 1 : 0 + count = var.enable_aibridge ? 1 : 0 agent_id = var.agent_id name = "ANTHROPIC_BASE_URL" value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" } resource "coder_env" "anthropic_auth_token" { - count = var.enable_coder_aibridge ? 1 : 0 + count = var.enable_aibridge ? 1 : 0 agent_id = var.agent_id name = "ANTHROPIC_AUTH_TOKEN" value = data.coder_workspace_owner.me.session_token From 3328805246c1a18d6a662cad05e025616002d634 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Wed, 14 Jan 2026 13:28:55 +0000 Subject: [PATCH 05/28] add validation block --- registry/coder/modules/claude-code/main.tf | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index eb7648000..87ba3cbc1 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -220,18 +220,15 @@ variable "enable_aibridge" { type = bool description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" default = false -} -resource "terraform_data" "validate_aibridge_auth" { - lifecycle { - precondition { - condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) - error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." - } - precondition { - condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) - error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." - } + validation { + condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) + error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." + } + + validation { + condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) + error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." } } From 4aad1956dde06f79004937dc1310188a5becfcba Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 05:09:20 +0000 Subject: [PATCH 06/28] feat: add ARG_ENABLE_AIBRIDGE support to installation script and configuration --- registry/coder/modules/claude-code/main.tf | 2 ++ .../modules/claude-code/scripts/install.sh | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 87ba3cbc1..33a62295e 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -281,6 +281,7 @@ resource "coder_env" "anthropic_base_url" { value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" } +# https://code.claude.com/docs/en/settings#environment-variables resource "coder_env" "anthropic_auth_token" { count = var.enable_aibridge ? 1 : 0 agent_id = var.agent_id @@ -388,6 +389,7 @@ module "agentapi" { ARG_ALLOWED_TOOLS='${var.allowed_tools}' \ ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \ ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ + ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ /tmp/install.sh EOT } diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 15981e8b5..f11402f0a 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -16,6 +16,7 @@ ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d) ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} +ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} echo "--------------------------------" @@ -27,6 +28,7 @@ printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" printf "ARG_MCP: %s\n" "$ARG_MCP" printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS" printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" +printf "ARG_ENABLE_AIBRIDGE %s\n" "$ARG_ENABLE_AIBRIDGE" echo "--------------------------------" @@ -83,8 +85,8 @@ function setup_claude_configurations() { function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." - if [ -z "${CLAUDE_API_KEY:-}" ]; then - echo "Note: CLAUDE_API_KEY not set, skipping authentication setup" + if [ -z "${CLAUDE_API_KEY:-}" ] && [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then + echo "Note: CLAUDE_API_KEY or enable_aibridge not set, skipping authentication setup" return fi @@ -92,18 +94,16 @@ function configure_standalone_mode() { local workdir_normalized workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - # Create or update .claude.json with minimal configuration for API key auth + # Create or update .claude.json with minimal configuration # This skips the interactive login prompt and onboarding screens if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" - jq --arg apikey "${CLAUDE_API_KEY:-}" \ - --arg workdir "$ARG_WORKDIR" \ + jq --arg workdir "$ARG_WORKDIR" \ '.autoUpdaterStatus = "disabled" | .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | .hasCompletedOnboarding = true | - .primaryApiKey = $apikey | .projects[$workdir].hasCompletedProjectOnboarding = true | .projects[$workdir].hasTrustDialogAccepted = true' \ "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" @@ -115,7 +115,6 @@ function configure_standalone_mode() { "bypassPermissionsModeAccepted": true, "hasAcknowledgedCostThreshold": true, "hasCompletedOnboarding": true, - "primaryApiKey": "${CLAUDE_API_KEY:-}", "projects": { "$ARG_WORKDIR": { "hasCompletedProjectOnboarding": true, @@ -126,6 +125,11 @@ function configure_standalone_mode() { EOF fi + # Add API key only if set + if [ -n "${CLAUDE_API_KEY:-}" ]; then + jq --arg apikey "${CLAUDE_API_KEY}" '.primaryApiKey = $apikey' "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" + fi + echo "Standalone mode configured successfully" } From 0ed44f49c1f32bbcfb3ad7bfe1ab86e51856f997 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 05:35:48 +0000 Subject: [PATCH 07/28] feat: restore tests --- .../coder/modules/claude-code/main.test.ts | 786 +++++++++--------- 1 file changed, 393 insertions(+), 393 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index d70abba0d..cff46c191 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -73,399 +73,399 @@ describe("claude-code", async () => { await runTerraformInit(import.meta.dir); }); - // test("happy-path", async () => { - // const { id } = await setup(); - // await execModuleScript(id); - // await expectAgentAPIStarted(id); - // }); - // - // test("install-claude-code-version", async () => { - // const version_to_install = "1.0.40"; - // const { id, coderEnvVars } = await setup({ - // skipClaudeMock: true, - // moduleVariables: { - // install_claude_code: "true", - // claude_code_version: version_to_install, - // }, - // }); - // await execModuleScript(id, coderEnvVars); - // const resp = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/install.log", - // ]); - // expect(resp.stdout).toContain(version_to_install); - // }); - // - // test("check-latest-claude-code-version-works", async () => { - // const { id, coderEnvVars } = await setup({ - // skipClaudeMock: true, - // skipAgentAPIMock: true, - // moduleVariables: { - // install_claude_code: "true", - // }, - // }); - // await execModuleScript(id, coderEnvVars); - // await expectAgentAPIStarted(id); - // }); - // - // test("claude-api-key", async () => { - // const apiKey = "test-api-key-123"; - // const { id } = await setup({ - // moduleVariables: { - // claude_api_key: apiKey, - // }, - // }); - // await execModuleScript(id); - // - // const envCheck = await execContainer(id, [ - // "bash", - // "-c", - // 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', - // ]); - // expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); - // }); - // - // test("claude-mcp-config", async () => { - // const mcpConfig = JSON.stringify({ - // mcpServers: { - // test: { - // command: "test-cmd", - // type: "stdio", - // }, - // }, - // }); - // const { id, coderEnvVars } = await setup({ - // skipClaudeMock: true, - // moduleVariables: { - // mcp: mcpConfig, - // }, - // }); - // await execModuleScript(id, coderEnvVars); - // - // const resp = await readFileContainer(id, "/home/coder/.claude.json"); - // expect(resp).toContain("test-cmd"); - // }); - // - // test("claude-task-prompt", async () => { - // const prompt = "This is a task prompt for Claude."; - // const { id } = await setup({ - // moduleVariables: { - // ai_prompt: prompt, - // }, - // }); - // await execModuleScript(id); - // - // const resp = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // expect(resp.stdout).toContain(prompt); - // }); - // - // test("claude-permission-mode", async () => { - // const mode = "plan"; - // const { id } = await setup({ - // moduleVariables: { - // permission_mode: mode, - // ai_prompt: "test prompt", - // }, - // }); - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // expect(startLog.stdout).toContain(`--permission-mode ${mode}`); - // }); - // - // test("claude-model", async () => { - // const model = "opus"; - // const { id } = await setup({ - // moduleVariables: { - // model: model, - // ai_prompt: "test prompt", - // }, - // }); - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // expect(startLog.stdout).toContain(`--model ${model}`); - // }); - // - // test("claude-continue-resume-task-session", async () => { - // const { id } = await setup({ - // moduleVariables: { - // continue: "true", - // report_tasks: "true", - // ai_prompt: "test prompt", - // }, - // }); - // - // // Create a mock task session file with the hardcoded task session ID - // // Note: Claude CLI creates files without "session-" prefix when using --session-id - // const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - // await execContainer(id, ["mkdir", "-p", sessionDir]); - // await execContainer(id, [ - // "bash", - // "-c", - // `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' - // {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} - // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} - // SESSIONEOF`, - // ]); - // - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // expect(startLog.stdout).toContain("--resume"); - // expect(startLog.stdout).toContain(taskSessionId); - // expect(startLog.stdout).toContain("Resuming task session"); - // expect(startLog.stdout).toContain("--dangerously-skip-permissions"); - // }); - // - // test("pre-post-install-scripts", async () => { - // const { id } = await setup({ - // moduleVariables: { - // pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", - // post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", - // }, - // }); - // await execModuleScript(id); - // - // const preInstallLog = await readFileContainer( - // id, - // "/home/coder/.claude-module/pre_install.log", - // ); - // expect(preInstallLog).toContain("claude-pre-install-script"); - // - // const postInstallLog = await readFileContainer( - // id, - // "/home/coder/.claude-module/post_install.log", - // ); - // expect(postInstallLog).toContain("claude-post-install-script"); - // }); - // - // test("workdir-variable", async () => { - // const workdir = "/home/coder/claude-test-folder"; - // const { id } = await setup({ - // skipClaudeMock: false, - // moduleVariables: { - // workdir, - // }, - // }); - // await execModuleScript(id); - // - // const resp = await readFileContainer( - // id, - // "/home/coder/.claude-module/agentapi-start.log", - // ); - // expect(resp).toContain(workdir); - // }); - // - // test("coder-mcp-config-created", async () => { - // const { id } = await setup({ - // moduleVariables: { - // install_claude_code: "false", - // }, - // }); - // await execModuleScript(id); - // - // const installLog = await readFileContainer( - // id, - // "/home/coder/.claude-module/install.log", - // ); - // expect(installLog).toContain( - // "Configuring Claude Code to report tasks via Coder MCP", - // ); - // }); - // - // test("dangerously-skip-permissions", async () => { - // const { id } = await setup({ - // moduleVariables: { - // dangerously_skip_permissions: "true", - // }, - // }); - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); - // }); - // - // test("subdomain-false", async () => { - // const { id } = await setup({ - // skipAgentAPIMock: true, - // moduleVariables: { - // subdomain: "false", - // post_install_script: dedent` - // #!/bin/bash - // env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" - // `, - // }, - // }); - // - // await execModuleScript(id); - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/post_install.log", - // ]); - // expect(startLog.stdout).toContain( - // "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", - // ); - // }); - // - // test("partial-initialization-detection", async () => { - // const { id } = await setup({ - // moduleVariables: { - // continue: "true", - // report_tasks: "true", - // ai_prompt: "test prompt", - // }, - // }); - // - // const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - // await execContainer(id, ["mkdir", "-p", sessionDir]); - // - // await execContainer(id, [ - // "bash", - // "-c", - // `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, - // ]); - // - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // - // // Should start new session, not try to resume invalid one - // expect(startLog.stdout).toContain("Starting new task session"); - // expect(startLog.stdout).toContain("--session-id"); - // }); - // - // test("standalone-first-build-no-sessions", async () => { - // const { id } = await setup({ - // moduleVariables: { - // continue: "true", - // report_tasks: "false", - // }, - // }); - // - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // - // // Should start fresh, not try to continue - // expect(startLog.stdout).toContain("No sessions found"); - // expect(startLog.stdout).toContain("starting fresh standalone session"); - // expect(startLog.stdout).not.toContain("--continue"); - // }); - // - // test("standalone-with-sessions-continues", async () => { - // const { id } = await setup({ - // moduleVariables: { - // continue: "true", - // report_tasks: "false", - // }, - // }); - // - // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - // await execContainer(id, ["mkdir", "-p", sessionDir]); - // await execContainer(id, [ - // "bash", - // "-c", - // `cat > ${sessionDir}/generic-123.jsonl << 'EOF' - // {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} - // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} - // EOF`, - // ]); - // - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // - // // Should continue existing session - // expect(startLog.stdout).toContain("Sessions found"); - // expect(startLog.stdout).toContain( - // "Continuing most recent standalone session", - // ); - // expect(startLog.stdout).toContain("--continue"); - // }); - // - // test("task-mode-ignores-manual-sessions", async () => { - // const { id } = await setup({ - // moduleVariables: { - // continue: "true", - // report_tasks: "true", - // ai_prompt: "test prompt", - // }, - // }); - // - // const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - // const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - // await execContainer(id, ["mkdir", "-p", sessionDir]); - // - // // Create task session (without "session-" prefix, as CLI does) - // await execContainer(id, [ - // "bash", - // "-c", - // `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' - // {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} - // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} - // EOF`, - // ]); - // - // // Create manual session (newer) - // await execContainer(id, [ - // "bash", - // "-c", - // `cat > ${sessionDir}/manual-456.jsonl << 'EOF' - // {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} - // {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} - // EOF`, - // ]); - // - // await execModuleScript(id); - // - // const startLog = await execContainer(id, [ - // "bash", - // "-c", - // "cat /home/coder/.claude-module/agentapi-start.log", - // ]); - // - // // Should resume task session, not manual session - // expect(startLog.stdout).toContain("Resuming task session"); - // expect(startLog.stdout).toContain(taskSessionId); - // expect(startLog.stdout).not.toContain("manual-456"); - // }); + test("happy-path", async () => { + const { id } = await setup(); + await execModuleScript(id); + await expectAgentAPIStarted(id); + }); + + test("install-claude-code-version", async () => { + const version_to_install = "1.0.40"; + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + moduleVariables: { + install_claude_code: "true", + claude_code_version: version_to_install, + }, + }); + await execModuleScript(id, coderEnvVars); + const resp = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/install.log", + ]); + expect(resp.stdout).toContain(version_to_install); + }); + + test("check-latest-claude-code-version-works", async () => { + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + skipAgentAPIMock: true, + moduleVariables: { + install_claude_code: "true", + }, + }); + await execModuleScript(id, coderEnvVars); + await expectAgentAPIStarted(id); + }); + + test("claude-api-key", async () => { + const apiKey = "test-api-key-123"; + const { id } = await setup({ + moduleVariables: { + claude_api_key: apiKey, + }, + }); + await execModuleScript(id); + + const envCheck = await execContainer(id, [ + "bash", + "-c", + 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', + ]); + expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); + }); + + test("claude-mcp-config", async () => { + const mcpConfig = JSON.stringify({ + mcpServers: { + test: { + command: "test-cmd", + type: "stdio", + }, + }, + }); + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + moduleVariables: { + mcp: mcpConfig, + }, + }); + await execModuleScript(id, coderEnvVars); + + const resp = await readFileContainer(id, "/home/coder/.claude.json"); + expect(resp).toContain("test-cmd"); + }); + + test("claude-task-prompt", async () => { + const prompt = "This is a task prompt for Claude."; + const { id } = await setup({ + moduleVariables: { + ai_prompt: prompt, + }, + }); + await execModuleScript(id); + + const resp = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(resp.stdout).toContain(prompt); + }); + + test("claude-permission-mode", async () => { + const mode = "plan"; + const { id } = await setup({ + moduleVariables: { + permission_mode: mode, + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--permission-mode ${mode}`); + }); + + test("claude-model", async () => { + const model = "opus"; + const { id } = await setup({ + moduleVariables: { + model: model, + ai_prompt: "test prompt", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--model ${model}`); + }); + + test("claude-continue-resume-task-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + // Create a mock task session file with the hardcoded task session ID + // Note: Claude CLI creates files without "session-" prefix when using --session-id + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' + {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} + {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} + SESSIONEOF`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("--resume"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).toContain("Resuming task session"); + expect(startLog.stdout).toContain("--dangerously-skip-permissions"); + }); + + test("pre-post-install-scripts", async () => { + const { id } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", + }, + }); + await execModuleScript(id); + + const preInstallLog = await readFileContainer( + id, + "/home/coder/.claude-module/pre_install.log", + ); + expect(preInstallLog).toContain("claude-pre-install-script"); + + const postInstallLog = await readFileContainer( + id, + "/home/coder/.claude-module/post_install.log", + ); + expect(postInstallLog).toContain("claude-post-install-script"); + }); + + test("workdir-variable", async () => { + const workdir = "/home/coder/claude-test-folder"; + const { id } = await setup({ + skipClaudeMock: false, + moduleVariables: { + workdir, + }, + }); + await execModuleScript(id); + + const resp = await readFileContainer( + id, + "/home/coder/.claude-module/agentapi-start.log", + ); + expect(resp).toContain(workdir); + }); + + test("coder-mcp-config-created", async () => { + const { id } = await setup({ + moduleVariables: { + install_claude_code: "false", + }, + }); + await execModuleScript(id); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + expect(installLog).toContain( + "Configuring Claude Code to report tasks via Coder MCP", + ); + }); + + test("dangerously-skip-permissions", async () => { + const { id } = await setup({ + moduleVariables: { + dangerously_skip_permissions: "true", + }, + }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); + }); + + test("subdomain-false", async () => { + const { id } = await setup({ + skipAgentAPIMock: true, + moduleVariables: { + subdomain: "false", + post_install_script: dedent` + #!/bin/bash + env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" + `, + }, + }); + + await execModuleScript(id); + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/post_install.log", + ]); + expect(startLog.stdout).toContain( + "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", + ); + }); + + test("partial-initialization-detection", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + + await execContainer(id, [ + "bash", + "-c", + `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should start new session, not try to resume invalid one + expect(startLog.stdout).toContain("Starting new task session"); + expect(startLog.stdout).toContain("--session-id"); + }); + + test("standalone-first-build-no-sessions", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + }, + }); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should start fresh, not try to continue + expect(startLog.stdout).toContain("No sessions found"); + expect(startLog.stdout).toContain("starting fresh standalone session"); + expect(startLog.stdout).not.toContain("--continue"); + }); + + test("standalone-with-sessions-continues", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + }, + }); + + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/generic-123.jsonl << 'EOF' + {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} + {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} + EOF`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should continue existing session + expect(startLog.stdout).toContain("Sessions found"); + expect(startLog.stdout).toContain( + "Continuing most recent standalone session", + ); + expect(startLog.stdout).toContain("--continue"); + }); + + test("task-mode-ignores-manual-sessions", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + + // Create task session (without "session-" prefix, as CLI does) + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' + {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} + {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} + EOF`, + ]); + + // Create manual session (newer) + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/manual-456.jsonl << 'EOF' + {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} + {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} + EOF`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should resume task session, not manual session + expect(startLog.stdout).toContain("Resuming task session"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).not.toContain("manual-456"); + }); test("claude-with-aibridge", async () => { const { id, coderEnvVars } = await setup({ From 3dfd968af1a75742455c80efea9aa31aa010cfcf Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 05:45:15 +0000 Subject: [PATCH 08/28] bun fmt --- .../coder/modules/claude-code/main.test.ts | 630 +++++++++--------- 1 file changed, 315 insertions(+), 315 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index fe042b7a5..ad17e832a 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -73,114 +73,114 @@ describe("claude-code", async () => { await runTerraformInit(import.meta.dir); }); - test("happy-path", async () => { - const { id } = await setup(); - await execModuleScript(id); - await expectAgentAPIStarted(id); + test("happy-path", async () => { + const { id } = await setup(); + await execModuleScript(id); + await expectAgentAPIStarted(id); + }); + + test("install-claude-code-version", async () => { + const version_to_install = "1.0.40"; + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + moduleVariables: { + install_claude_code: "true", + claude_code_version: version_to_install, + }, }); + await execModuleScript(id, coderEnvVars); + const resp = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/install.log", + ]); + expect(resp.stdout).toContain(version_to_install); + }); - test("install-claude-code-version", async () => { - const version_to_install = "1.0.40"; - const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, - moduleVariables: { - install_claude_code: "true", - claude_code_version: version_to_install, - }, - }); - await execModuleScript(id, coderEnvVars); - const resp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/install.log", - ]); - expect(resp.stdout).toContain(version_to_install); + test("check-latest-claude-code-version-works", async () => { + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + skipAgentAPIMock: true, + moduleVariables: { + install_claude_code: "true", + }, }); + await execModuleScript(id, coderEnvVars); + await expectAgentAPIStarted(id); + }); - test("check-latest-claude-code-version-works", async () => { - const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, - skipAgentAPIMock: true, - moduleVariables: { - install_claude_code: "true", - }, - }); - await execModuleScript(id, coderEnvVars); - await expectAgentAPIStarted(id); + test("claude-api-key", async () => { + const apiKey = "test-api-key-123"; + const { id } = await setup({ + moduleVariables: { + claude_api_key: apiKey, + }, }); + await execModuleScript(id); - test("claude-api-key", async () => { - const apiKey = "test-api-key-123"; - const { id } = await setup({ - moduleVariables: { - claude_api_key: apiKey, + const envCheck = await execContainer(id, [ + "bash", + "-c", + 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', + ]); + expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); + }); + + test("claude-mcp-config", async () => { + const mcpConfig = JSON.stringify({ + mcpServers: { + test: { + command: "test-cmd", + type: "stdio", }, - }); - await execModuleScript(id); - - const envCheck = await execContainer(id, [ - "bash", - "-c", - 'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"', - ]); - expect(envCheck.stdout).toContain("CLAUDE_API_KEY"); + }, + }); + const { id, coderEnvVars } = await setup({ + skipClaudeMock: true, + moduleVariables: { + mcp: mcpConfig, + }, }); + await execModuleScript(id, coderEnvVars); - test("claude-mcp-config", async () => { - const mcpConfig = JSON.stringify({ - mcpServers: { - test: { - command: "test-cmd", - type: "stdio", - }, - }, - }); - const { id, coderEnvVars } = await setup({ - skipClaudeMock: true, - moduleVariables: { - mcp: mcpConfig, - }, - }); - await execModuleScript(id, coderEnvVars); + const resp = await readFileContainer(id, "/home/coder/.claude.json"); + expect(resp).toContain("test-cmd"); + }); - const resp = await readFileContainer(id, "/home/coder/.claude.json"); - expect(resp).toContain("test-cmd"); + test("claude-task-prompt", async () => { + const prompt = "This is a task prompt for Claude."; + const { id } = await setup({ + moduleVariables: { + ai_prompt: prompt, + }, }); + await execModuleScript(id); - test("claude-task-prompt", async () => { - const prompt = "This is a task prompt for Claude."; - const { id } = await setup({ - moduleVariables: { - ai_prompt: prompt, - }, - }); - await execModuleScript(id); - - const resp = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(resp.stdout).toContain(prompt); - }); + const resp = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(resp.stdout).toContain(prompt); + }); - test("claude-permission-mode", async () => { - const mode = "plan"; - const { id } = await setup({ - moduleVariables: { - permission_mode: mode, - ai_prompt: "test prompt", - }, - }); - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--permission-mode ${mode}`); + test("claude-permission-mode", async () => { + const mode = "plan"; + const { id } = await setup({ + moduleVariables: { + permission_mode: mode, + ai_prompt: "test prompt", + }, }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--permission-mode ${mode}`); + }); test("claude-model", async () => { const model = "opus"; @@ -195,272 +195,272 @@ describe("claude-code", async () => { expect(coderEnvVars["ANTHROPIC_MODEL"]).toBe(model); }); - test("claude-continue-resume-task-session", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); - - // Create a mock task session file with the hardcoded task session ID - // Note: Claude CLI creates files without "session-" prefix when using --session-id - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' + test("claude-continue-resume-task-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + // Create a mock task session file with the hardcoded task session ID + // Note: Claude CLI creates files without "session-" prefix when using --session-id + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} SESSIONEOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain("--resume"); - expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).toContain("Resuming task session"); - expect(startLog.stdout).toContain("--dangerously-skip-permissions"); - }); + ]); - test("pre-post-install-scripts", async () => { - const { id } = await setup({ - moduleVariables: { - pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", - post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", - }, - }); - await execModuleScript(id); - - const preInstallLog = await readFileContainer( - id, - "/home/coder/.claude-module/pre_install.log", - ); - expect(preInstallLog).toContain("claude-pre-install-script"); - - const postInstallLog = await readFileContainer( - id, - "/home/coder/.claude-module/post_install.log", - ); - expect(postInstallLog).toContain("claude-post-install-script"); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("--resume"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).toContain("Resuming task session"); + expect(startLog.stdout).toContain("--dangerously-skip-permissions"); + }); + + test("pre-post-install-scripts", async () => { + const { id } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'", + post_install_script: "#!/bin/bash\necho 'claude-post-install-script'", + }, }); + await execModuleScript(id); + + const preInstallLog = await readFileContainer( + id, + "/home/coder/.claude-module/pre_install.log", + ); + expect(preInstallLog).toContain("claude-pre-install-script"); + + const postInstallLog = await readFileContainer( + id, + "/home/coder/.claude-module/post_install.log", + ); + expect(postInstallLog).toContain("claude-post-install-script"); + }); - test("workdir-variable", async () => { - const workdir = "/home/coder/claude-test-folder"; - const { id } = await setup({ - skipClaudeMock: false, - moduleVariables: { - workdir, - }, - }); - await execModuleScript(id); - - const resp = await readFileContainer( - id, - "/home/coder/.claude-module/agentapi-start.log", - ); - expect(resp).toContain(workdir); + test("workdir-variable", async () => { + const workdir = "/home/coder/claude-test-folder"; + const { id } = await setup({ + skipClaudeMock: false, + moduleVariables: { + workdir, + }, }); + await execModuleScript(id); - test("coder-mcp-config-created", async () => { - const { id } = await setup({ - moduleVariables: { - install_claude_code: "false", - }, - }); - await execModuleScript(id); - - const installLog = await readFileContainer( - id, - "/home/coder/.claude-module/install.log", - ); - expect(installLog).toContain( - "Configuring Claude Code to report tasks via Coder MCP", - ); + const resp = await readFileContainer( + id, + "/home/coder/.claude-module/agentapi-start.log", + ); + expect(resp).toContain(workdir); + }); + + test("coder-mcp-config-created", async () => { + const { id } = await setup({ + moduleVariables: { + install_claude_code: "false", + }, }); + await execModuleScript(id); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + expect(installLog).toContain( + "Configuring Claude Code to report tasks via Coder MCP", + ); + }); - test("dangerously-skip-permissions", async () => { - const { id } = await setup({ - moduleVariables: { - dangerously_skip_permissions: "true", - }, - }); - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); + test("dangerously-skip-permissions", async () => { + const { id } = await setup({ + moduleVariables: { + dangerously_skip_permissions: "true", + }, }); + await execModuleScript(id); - test("subdomain-false", async () => { - const { id } = await setup({ - skipAgentAPIMock: true, - moduleVariables: { - subdomain: "false", - post_install_script: dedent` + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain(`--dangerously-skip-permissions`); + }); + + test("subdomain-false", async () => { + const { id } = await setup({ + skipAgentAPIMock: true, + moduleVariables: { + subdomain: "false", + post_install_script: dedent` #!/bin/bash env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" `, - }, - }); - - await execModuleScript(id); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/post_install.log", - ]); - expect(startLog.stdout).toContain( - "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", - ); + }, }); - test("partial-initialization-detection", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); + await execModuleScript(id); + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/post_install.log", + ]); + expect(startLog.stdout).toContain( + "ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat", + ); + }); + + test("partial-initialization-detection", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, - ]); + await execContainer(id, [ + "bash", + "-c", + `echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`, + ]); - await execModuleScript(id); + await execModuleScript(id); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + + // Should start new session, not try to resume invalid one + expect(startLog.stdout).toContain("Starting new task session"); + expect(startLog.stdout).toContain("--session-id"); + }); - // Should start new session, not try to resume invalid one - expect(startLog.stdout).toContain("Starting new task session"); - expect(startLog.stdout).toContain("--session-id"); + test("standalone-first-build-no-sessions", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + }, }); - test("standalone-first-build-no-sessions", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - }, - }); + await execModuleScript(id); - await execModuleScript(id); + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); + // Should start fresh, not try to continue + expect(startLog.stdout).toContain("No sessions found"); + expect(startLog.stdout).toContain("starting fresh standalone session"); + expect(startLog.stdout).not.toContain("--continue"); + }); - // Should start fresh, not try to continue - expect(startLog.stdout).toContain("No sessions found"); - expect(startLog.stdout).toContain("starting fresh standalone session"); - expect(startLog.stdout).not.toContain("--continue"); + test("standalone-with-sessions-continues", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + }, }); - test("standalone-with-sessions-continues", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "false", - }, - }); - - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/generic-123.jsonl << 'EOF' + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/generic-123.jsonl << 'EOF' {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} EOF`, - ]); - - await execModuleScript(id); - - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); - - // Should continue existing session - expect(startLog.stdout).toContain("Sessions found"); - expect(startLog.stdout).toContain( - "Continuing most recent standalone session", - ); - expect(startLog.stdout).toContain("--continue"); - }); + ]); - test("task-mode-ignores-manual-sessions", async () => { - const { id } = await setup({ - moduleVariables: { - continue: "true", - report_tasks: "true", - ai_prompt: "test prompt", - }, - }); + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); - const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; - const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; - await execContainer(id, ["mkdir", "-p", sessionDir]); + // Should continue existing session + expect(startLog.stdout).toContain("Sessions found"); + expect(startLog.stdout).toContain( + "Continuing most recent standalone session", + ); + expect(startLog.stdout).toContain("--continue"); + }); - // Create task session (without "session-" prefix, as CLI does) - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' + test("task-mode-ignores-manual-sessions", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "true", + ai_prompt: "test prompt", + }, + }); + + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + + // Create task session (without "session-" prefix, as CLI does) + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} EOF`, - ]); + ]); - // Create manual session (newer) - await execContainer(id, [ - "bash", - "-c", - `cat > ${sessionDir}/manual-456.jsonl << 'EOF' + // Create manual session (newer) + await execContainer(id, [ + "bash", + "-c", + `cat > ${sessionDir}/manual-456.jsonl << 'EOF' {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} EOF`, - ]); + ]); - await execModuleScript(id); + await execModuleScript(id); - const startLog = await execContainer(id, [ - "bash", - "-c", - "cat /home/coder/.claude-module/agentapi-start.log", - ]); + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); - // Should resume task session, not manual session - expect(startLog.stdout).toContain("Resuming task session"); - expect(startLog.stdout).toContain(taskSessionId); - expect(startLog.stdout).not.toContain("manual-456"); - }); + // Should resume task session, not manual session + expect(startLog.stdout).toContain("Resuming task session"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).not.toContain("manual-456"); + }); test("claude-with-aibridge", async () => { const { id, coderEnvVars } = await setup({ From 2e76f43b3aec2e9fd2f6ab036a5a506da063b238 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 05:50:28 +0000 Subject: [PATCH 09/28] bun fmt --- .../coder/modules/claude-code/main.test.ts | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index ad17e832a..4a57badb9 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -461,32 +461,4 @@ describe("claude-code", async () => { expect(startLog.stdout).toContain(taskSessionId); expect(startLog.stdout).not.toContain("manual-456"); }); - - test("claude-with-aibridge", async () => { - const { id, coderEnvVars } = await setup({ - moduleVariables: { - enable_aibridge: "true", - }, - }); - - console.error(coderEnvVars); - - await execModuleScript(id, coderEnvVars); - - // Check environment variables are set correctly - const envCheck = await execContainer(id, [ - "bash", - "-c", - 'env | grep ANTHROPIC_BASE_URL || echo "ANTHROPIC_BASE_URL not found"', - ]); - expect(envCheck.stdout).toContain("ANTHROPIC_BASE_URL"); - expect(envCheck.stdout).toContain("/api/v2/aibridge/anthropic"); - - const authTokenCheck = await execContainer(id, [ - "bash", - "-c", - 'env | grep ANTHROPIC_AUTH_TOKEN || echo "ANTHROPIC_AUTH_TOKEN not found"', - ]); - expect(authTokenCheck.stdout).toContain("ANTHROPIC_AUTH_TOKEN"); - }); }); From a59a423c6d46056170ae7264ab56cce8941fecde Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 05:54:01 +0000 Subject: [PATCH 10/28] bun fmt --- .../coder/modules/claude-code/main.test.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 4a57badb9..d59b6a8f4 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -213,9 +213,9 @@ describe("claude-code", async () => { "bash", "-c", `cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF' - {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} - {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} - SESSIONEOF`, +{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} +SESSIONEOF`, ]); await execModuleScript(id); @@ -309,9 +309,9 @@ describe("claude-code", async () => { moduleVariables: { subdomain: "false", post_install_script: dedent` - #!/bin/bash - env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" - `, + #!/bin/bash + env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found" + `, }, }); @@ -394,9 +394,9 @@ describe("claude-code", async () => { "bash", "-c", `cat > ${sessionDir}/generic-123.jsonl << 'EOF' - {"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} - {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} - EOF`, +{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} +EOF`, ]); await execModuleScript(id); @@ -433,9 +433,9 @@ describe("claude-code", async () => { "bash", "-c", `cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF' - {"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} - {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} - EOF`, +{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"} +EOF`, ]); // Create manual session (newer) @@ -443,9 +443,9 @@ describe("claude-code", async () => { "bash", "-c", `cat > ${sessionDir}/manual-456.jsonl << 'EOF' - {"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} - {"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} - EOF`, +{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"} +{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"} +EOF`, ]); await execModuleScript(id); From 28fc496ffc21b794df2c9bc385b75bebfbac29a7 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 05:59:09 +0000 Subject: [PATCH 11/28] bun fmt --- registry/coder/modules/claude-code/README.md | 74 ++++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 958b87bf0..a2b38db31 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -136,6 +136,50 @@ module "claude-code" { } ``` +### Usage with AI Bridge Configuration + +For AI Bridge configuration set `enable_aibridge` to `true`. [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. + +#### Usage with tasks and AI Bridge + +```tf +resource "coder_ai_task" "task" { + count = data.coder_workspace.me.start_count + app_id = module.claude-code.task_app_id +} + +data "coder_task" "me" {} + +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.4.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + ai_prompt = data.coder_task.me.prompt + enable_aibridge = true +} +``` + +#### Standalone usage and AI Bridge + +```tf +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.4.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_aibridge = true +} +``` + +When `enable_aibridge = true`, the module automatically sets: + +- `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` +- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token + +This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. +Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. + ### Usage with AWS Bedrock #### Prerequisites @@ -291,36 +335,6 @@ module "claude-code" { > [!NOTE] > For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai). -### AI Bridge Configuration - -For AI Bridge configuration set `enable_aibridge` to `true`. [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. - -```tf -resource "coder_ai_task" "task" { - count = data.coder_workspace.me.start_count - app_id = module.claude-code.task_app_id -} - -data "coder_task" "me" {} - -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.0" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - ai_prompt = data.coder_task.me.prompt - enable_aibridge = true -} -``` - -When `enable_aibridge = true`, the module automatically sets: - -- `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` -- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token - -This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. -Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. - ## Troubleshooting If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information. From 8a4b5e3ef1eec9184c007e52c06b54d279f8ae01 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 06:02:29 +0000 Subject: [PATCH 12/28] bump version --- registry/coder/modules/claude-code/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index a2b38db31..aaaab0a0c 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -70,7 +70,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -107,7 +107,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -152,7 +152,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.0" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -165,7 +165,7 @@ module "claude-code" { ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.0" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -246,7 +246,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -303,7 +303,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.1" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" From f40c16de00b1f8027f5bde5f977cc581578acca5 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 15 Jan 2026 14:36:48 +0000 Subject: [PATCH 13/28] feat: update validation error message --- registry/coder/modules/claude-code/README.md | 2 +- registry/coder/modules/claude-code/main.tf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index aaaab0a0c..1ac35872c 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -160,7 +160,7 @@ module "claude-code" { } ``` -#### Standalone usage and AI Bridge +#### Standalone usage with AI Bridge ```tf module "claude-code" { diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 5455b71d5..cdbadb681 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -235,12 +235,12 @@ variable "enable_aibridge" { validation { condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) - error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." + error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using their Coder credentials." } validation { condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) - error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge uses Coder's authentication." + error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using their Coder credentials." } } From 64a790ceea6257f64dcdd7067e426161145029b9 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Fri, 16 Jan 2026 15:42:33 +0000 Subject: [PATCH 14/28] chore: update README.md --- registry/coder/modules/claude-code/README.md | 54 ++++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 1ac35872c..f7e7353e7 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -53,12 +53,9 @@ module "claude-code" { } ``` -### Usage with Tasks and Advanced Configuration +### Usage with Tasks -This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings. - -> [!NOTE] -> When a specific `claude_code_version` (other than "latest") is provided, the module will install Claude Code via npm instead of the official installer. This allows for version pinning. The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. +This example shows how to configure Claude Code with Coder tasks. ```tf resource "coder_ai_task" "task" { @@ -68,6 +65,27 @@ resource "coder_ai_task" "task" { data "coder_task" "me" {} +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.5.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + claude_api_key = "xxxx-xxxxx-xxxx" + ai_prompt = data.coder_task.me.prompt + + # Optional: route through AI Bridge (Premium feature) + # enable_aibridge = true +} +``` + +### Advanced Configuration + +This example shows additional configuration options for version pinning, custom models, and MCP servers. + +> [!NOTE] +> When a specific `claude_code_version` (other than "latest") is provided, the module will install Claude Code via npm instead of the official installer. This allows for version pinning. The `claude_binary_path` variable can be used to specify where a pre-installed Claude binary is located. + +```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" version = "4.5.0" @@ -82,9 +100,7 @@ module "claude-code" { claude_binary_path = "/opt/claude/bin" # Path to pre-installed Claude binary agentapi_version = "0.11.4" - ai_prompt = data.coder_task.me.prompt - model = "sonnet" - + model = "sonnet" permission_mode = "plan" mcp = <<-EOF @@ -138,27 +154,9 @@ module "claude-code" { ### Usage with AI Bridge Configuration -For AI Bridge configuration set `enable_aibridge` to `true`. [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. - -#### Usage with tasks and AI Bridge - -```tf -resource "coder_ai_task" "task" { - count = data.coder_workspace.me.start_count - app_id = module.claude-code.task_app_id -} - -data "coder_task" "me" {} +[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - ai_prompt = data.coder_task.me.prompt - enable_aibridge = true -} -``` +For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example above. #### Standalone usage with AI Bridge From d6e77aec79c2c71c72328b730db94eb72b2de6d4 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Sat, 17 Jan 2026 17:38:56 +0530 Subject: [PATCH 15/28] chore: rearrange README.md --- registry/coder/modules/claude-code/README.md | 52 ++++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index f7e7353e7..b637dbf7d 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -53,6 +53,32 @@ module "claude-code" { } ``` +### Usage with AI Bridge Configuration + +[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. + +For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example below. + +#### Standalone usage with AI Bridge + +```tf +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.5.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_aibridge = true +} +``` + +When `enable_aibridge = true`, the module automatically sets: + +- `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` +- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token + +This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. +Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. + ### Usage with Tasks This example shows how to configure Claude Code with Coder tasks. @@ -152,32 +178,6 @@ module "claude-code" { } ``` -### Usage with AI Bridge Configuration - -[AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. - -For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage with Tasks](#usage-with-tasks) example above. - -#### Standalone usage with AI Bridge - -```tf -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.5.0" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - enable_aibridge = true -} -``` - -When `enable_aibridge = true`, the module automatically sets: - -- `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` -- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token - -This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. -Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. - ### Usage with AWS Bedrock #### Prerequisites From 7829d117c44431ca1a2e7b0f634d9c08c395d709 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:03:39 +0530 Subject: [PATCH 16/28] Update registry/coder/modules/claude-code/README.md Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index b637dbf7d..f7c544f9d 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -53,7 +53,7 @@ module "claude-code" { } ``` -### Usage with AI Bridge Configuration +### Usage with AI Bridge [AI Bridge](https://coder.com/docs/ai-coder/ai-bridge) is a Premium Coder feature that provides centralized LLM proxy management. To use AI Bridge, set `enable_aibridge = true`. From 85a83f1e05ad8b51642bbbd414436f739314ec5d Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 18:29:21 +0530 Subject: [PATCH 17/28] chore: test --- registry/coder/modules/claude-code/scripts/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index a909e7676..ce17df48b 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -176,9 +176,9 @@ EOF fi # Add API key only if set - if [ -n "${CLAUDE_API_KEY:-}" ]; then +# if [ -n "${CLAUDE_API_KEY:-}" ]; then jq --arg apikey "${CLAUDE_API_KEY}" '.primaryApiKey = $apikey' "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" - fi +# fi echo "Standalone mode configured successfully" } From 40fc90d0521621f73465a17aeb424956841cce89 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 18:55:41 +0530 Subject: [PATCH 18/28] chore: test --- registry/coder/modules/claude-code/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index cdbadb681..271e57d98 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -308,7 +308,7 @@ resource "coder_env" "anthropic_base_url" { resource "coder_env" "anthropic_auth_token" { count = var.enable_aibridge ? 1 : 0 agent_id = var.agent_id - name = "ANTHROPIC_AUTH_TOKEN" + name = "CLAUDE_API_KEY" value = data.coder_workspace_owner.me.session_token } From 08bebf1ddb8fd1d711b1168395da70b8c4f7917d Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:15:19 +0530 Subject: [PATCH 19/28] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/claude-code/scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index ce17df48b..117e63556 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -32,7 +32,7 @@ printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG" printf "ARG_MCP: %s\n" "$ARG_MCP" printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS" printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS" -printf "ARG_ENABLE_AIBRIDGE %s\n" "$ARG_ENABLE_AIBRIDGE" +printf "ARG_ENABLE_AIBRIDGE: %s\n" "$ARG_ENABLE_AIBRIDGE" echo "--------------------------------" From c5f8ffce9bbd1f190b51054f8063ef47e1e6b1b1 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:15:58 +0530 Subject: [PATCH 20/28] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/claude-code/scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 117e63556..1b9b519af 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -136,7 +136,7 @@ function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." if [ -z "${CLAUDE_API_KEY:-}" ] && [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then - echo "Note: CLAUDE_API_KEY or enable_aibridge not set, skipping authentication setup" + echo "Note: Neither CLAUDE_API_KEY nor enable_aibridge is set, skipping authentication setup" return fi From c8c01c83a35a9e96c7267bfcb6aa19949fe2e0d2 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 19:19:37 +0530 Subject: [PATCH 21/28] feat: update Claude Code authentication to support AI Bridge integration --- registry/coder/modules/claude-code/README.md | 2 +- registry/coder/modules/claude-code/main.tf | 11 +---------- registry/coder/modules/claude-code/scripts/install.sh | 11 ++++------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index f7c544f9d..eff7dfdd1 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -74,7 +74,7 @@ module "claude-code" { When `enable_aibridge = true`, the module automatically sets: - `ANTHROPIC_BASE_URL` to `${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic` -- `ANTHROPIC_AUTH_TOKEN` to the workspace owner's session token +- `CLAUDE_API_KEY` to the workspace owner's session token This allows Claude Code to route API requests through Coder's AI Bridge instead of directly to Anthropic's API. Template build will fail if either `claude_api_key` or `claude_code_oauth_token` is provided alongside `enable_aibridge = true`. diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 271e57d98..5edd2c84d 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -264,10 +264,9 @@ resource "coder_env" "claude_code_oauth_token" { } resource "coder_env" "claude_api_key" { - count = length(var.claude_api_key) > 0 ? 1 : 0 agent_id = var.agent_id name = "CLAUDE_API_KEY" - value = var.claude_api_key + value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key } resource "coder_env" "disable_autoupdater" { @@ -304,14 +303,6 @@ resource "coder_env" "anthropic_base_url" { value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" } -# https://code.claude.com/docs/en/settings#environment-variables -resource "coder_env" "anthropic_auth_token" { - count = var.enable_aibridge ? 1 : 0 - agent_id = var.agent_id - name = "CLAUDE_API_KEY" - value = data.coder_workspace_owner.me.session_token -} - locals { # we have to trim the slash because otherwise coder exp mcp will # set up an invalid claude config diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 1b9b519af..1f9880571 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -136,7 +136,7 @@ function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." if [ -z "${CLAUDE_API_KEY:-}" ] && [ "$ARG_ENABLE_AIBRIDGE" = "false" ]; then - echo "Note: Neither CLAUDE_API_KEY nor enable_aibridge is set, skipping authentication setup" + echo "Note: Neither claude_api_key nor enable_aibridge is set, skipping authentication setup" return fi @@ -144,7 +144,7 @@ function configure_standalone_mode() { local workdir_normalized workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') - # Create or update .claude.json with minimal configuration + # Create or update .claude.json with minimal configuration for API key auth # This skips the interactive login prompt and onboarding screens if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" @@ -154,6 +154,7 @@ function configure_standalone_mode() { .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | .hasCompletedOnboarding = true | + .primaryApiKey = $apikey | .projects[$workdir].hasCompletedProjectOnboarding = true | .projects[$workdir].hasTrustDialogAccepted = true' \ "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" @@ -165,6 +166,7 @@ function configure_standalone_mode() { "bypassPermissionsModeAccepted": true, "hasAcknowledgedCostThreshold": true, "hasCompletedOnboarding": true, + "primaryApiKey": "${CLAUDE_API_KEY:-}", "projects": { "$ARG_WORKDIR": { "hasCompletedProjectOnboarding": true, @@ -175,11 +177,6 @@ function configure_standalone_mode() { EOF fi - # Add API key only if set -# if [ -n "${CLAUDE_API_KEY:-}" ]; then - jq --arg apikey "${CLAUDE_API_KEY}" '.primaryApiKey = $apikey' "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" -# fi - echo "Standalone mode configured successfully" } From dce4cff97f0abcd1bb0530882f18103d2860b2d3 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:20:32 +0530 Subject: [PATCH 22/28] Update registry/coder/modules/claude-code/main.tf Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 5edd2c84d..9183c365a 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -235,7 +235,7 @@ variable "enable_aibridge" { validation { condition = !(var.enable_aibridge && length(var.claude_api_key) > 0) - error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using their Coder credentials." + error_message = "claude_api_key cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." } validation { From 495fdd1dcfb316e34846a4a67f844edda7a48e45 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:20:43 +0530 Subject: [PATCH 23/28] Update registry/coder/modules/claude-code/main.tf Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 9183c365a..dd5402790 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -240,7 +240,7 @@ variable "enable_aibridge" { validation { condition = !(var.enable_aibridge && length(var.claude_code_oauth_token) > 0) - error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using their Coder credentials." + error_message = "claude_code_oauth_token cannot be provided when enable_aibridge is true. AI Bridge automatically authenticates the client using Coder credentials." } } From a866b16f29c0bdec249ab170fcebdce6337288d2 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 19:33:06 +0530 Subject: [PATCH 24/28] feat: add terraform validation tests --- .../coder/modules/claude-code/main.tftest.hcl | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index dd9e66a68..55106170f 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -42,7 +42,7 @@ run "test_claude_code_with_api_key" { } assert { - condition = coder_env.claude_api_key[0].value == "test-api-key-123" + condition = coder_env.claude_api_key.value == "test-api-key-123" error_message = "Claude API key value should match the input" } } @@ -288,3 +288,94 @@ run "test_claude_report_tasks_disabled" { error_message = "System prompt should end with " } } + +run "test_aibridge_enabled" { + command = plan + + variables { + agent_id = "test-agent-aibridge" + workdir = "/home/coder/aibridge" + enable_aibridge = true + } + + assert { + condition = var.enable_aibridge == true + error_message = "AI Bridge should be enabled" + } + + assert { + condition = coder_env.anthropic_base_url[0].name == "ANTHROPIC_BASE_URL" + error_message = "ANTHROPIC_BASE_URL environment variable should be set" + } + + assert { + condition = length(regexall("/api/v2/aibridge/anthropic", coder_env.anthropic_base_url[0].value)) > 0 + error_message = "ANTHROPIC_BASE_URL should point to AI Bridge endpoint" + } + + assert { + condition = coder_env.claude_api_key.name == "CLAUDE_API_KEY" + error_message = "CLAUDE_API_KEY environment variable should be set" + } + + assert { + condition = coder_env.claude_api_key.value == data.coder_workspace_owner.me.session_token + error_message = "CLAUDE_API_KEY should use workspace owner's session token when aibridge is enabled" + } +} + +run "test_aibridge_validation_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + enable_aibridge = true + claude_api_key = "test-api-key" + } + + expect_failures = [ + var.enable_aibridge, + ] +} + +run "test_aibridge_validation_with_oauth_token" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + enable_aibridge = true + claude_code_oauth_token = "test-oauth-token" + } + + expect_failures = [ + var.enable_aibridge, + ] +} + +run "test_aibridge_disabled_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-no-aibridge" + workdir = "/home/coder/test" + enable_aibridge = false + claude_api_key = "test-api-key-xyz" + } + + assert { + condition = var.enable_aibridge == false + error_message = "AI Bridge should be disabled" + } + + assert { + condition = coder_env.claude_api_key.value == "test-api-key-xyz" + error_message = "CLAUDE_API_KEY should use the provided API key when aibridge is disabled" + } + + assert { + condition = length(coder_env.anthropic_base_url) == 0 + error_message = "ANTHROPIC_BASE_URL should not be set when aibridge is disabled" + } +} From 2471c2fe13a1f79b37e5967a3609954d43e688a8 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 21:06:40 +0530 Subject: [PATCH 25/28] wip --- registry/coder/modules/claude-code/main.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index dd5402790..e4aff7bc1 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -269,6 +269,12 @@ resource "coder_env" "claude_api_key" { value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key } +resource "coder_env" "claude_api_key_re" { + agent_id = var.agent_id + name = "ANTHROPIC_API_KEY" + value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key +} + resource "coder_env" "disable_autoupdater" { count = var.disable_autoupdater ? 1 : 0 agent_id = var.agent_id From a22618a4d8d5fca64b0cb9e143da09e853228b7d Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 21:13:24 +0530 Subject: [PATCH 26/28] wip --- registry/coder/modules/claude-code/scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 1f9880571..25e8abb82 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -154,7 +154,7 @@ function configure_standalone_mode() { .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | .hasCompletedOnboarding = true | - .primaryApiKey = $apikey | + .primaryApiKey = "${CLAUDE_API_KEY:-}" | .projects[$workdir].hasCompletedProjectOnboarding = true | .projects[$workdir].hasTrustDialogAccepted = true' \ "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" From d86dca059a808f921bb63d1c58f4e127b144f298 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 21:18:16 +0530 Subject: [PATCH 27/28] wip --- registry/coder/modules/claude-code/scripts/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 25e8abb82..07e199c18 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -149,12 +149,12 @@ function configure_standalone_mode() { if [ -f "$claude_config" ]; then echo "Updating existing Claude configuration at $claude_config" - jq --arg workdir "$ARG_WORKDIR" \ + jq --arg workdir "$ARG_WORKDIR" --arg apikey "${CLAUDE_API_KEY:-}" \ '.autoUpdaterStatus = "disabled" | .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true | .hasCompletedOnboarding = true | - .primaryApiKey = "${CLAUDE_API_KEY:-}" | + .primaryApiKey = $apikey | .projects[$workdir].hasCompletedProjectOnboarding = true | .projects[$workdir].hasTrustDialogAccepted = true' \ "$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config" From 87e04fd642c2320cea134ecc2d766b810ece5fec Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 19 Jan 2026 21:26:00 +0530 Subject: [PATCH 28/28] wip --- registry/coder/modules/claude-code/main.tf | 6 ------ 1 file changed, 6 deletions(-) diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index e4aff7bc1..dd5402790 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -269,12 +269,6 @@ resource "coder_env" "claude_api_key" { value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key } -resource "coder_env" "claude_api_key_re" { - agent_id = var.agent_id - name = "ANTHROPIC_API_KEY" - value = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key -} - resource "coder_env" "disable_autoupdater" { count = var.disable_autoupdater ? 1 : 0 agent_id = var.agent_id