diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index ce7810a87..eff7dfdd1 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 @@ -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.2" + 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.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -53,25 +53,68 @@ module "claude-code" { } ``` -### Usage with Tasks and Advanced Configuration +### Usage with AI Bridge -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. +[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`. -> [!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. +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 -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" - default = "" - description = "Initial task prompt for Claude Code." - mutable = 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" + 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` +- `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`. + +### Usage with Tasks + +This example shows how to configure Claude Code with Coder tasks. + +```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.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.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -83,9 +126,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_parameter.ai_prompt.value - model = "sonnet" - + model = "sonnet" permission_mode = "plan" mcp = <<-EOF @@ -108,7 +149,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.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -130,7 +171,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -203,7 +244,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -260,7 +301,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.4.2" + version = "4.5.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index d0681af62..dd5402790 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -228,6 +228,22 @@ variable "compile_boundary_from_source" { default = false } +variable "enable_aibridge" { + type = bool + description = "Use AI Bridge for Claude Code. https://coder.com/docs/ai-coder/ai-bridge" + default = false + + 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 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 automatically authenticates the client using Coder credentials." + } +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 agent_id = var.agent_id @@ -248,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" { @@ -281,6 +296,13 @@ resource "coder_env" "anthropic_model" { value = var.model } +resource "coder_env" "anthropic_base_url" { + 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" +} + locals { # we have to trim the slash because otherwise coder exp mcp will # set up an invalid claude config @@ -382,6 +404,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/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" + } +} diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 5b9584cb4..07e199c18 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -18,6 +18,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 "--------------------------------" @@ -31,6 +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" echo "--------------------------------" @@ -133,8 +135,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: Neither claude_api_key nor enable_aibridge is set, skipping authentication setup" return fi @@ -147,8 +149,7 @@ function configure_standalone_mode() { 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" --arg apikey "${CLAUDE_API_KEY:-}" \ '.autoUpdaterStatus = "disabled" | .bypassPermissionsModeAccepted = true | .hasAcknowledgedCostThreshold = true |