diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 48b291bb0..c142ba424 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.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -60,7 +60,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -81,7 +81,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -110,7 +110,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -133,7 +133,7 @@ This example shows additional configuration options for version pinning, custom ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -189,7 +189,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.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -211,7 +211,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" + version = "4.9.3" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -220,99 +220,60 @@ module "claude-code" { ### Usage with AWS Bedrock -#### Prerequisites +Set `use_bedrock = true` to route Claude Code through Amazon Bedrock. The module sets `CLAUDE_CODE_USE_BEDROCK=1` and skips Anthropic API key setup; authentication is handled by the AWS SDK credential chain inside the workspace. -AWS account with Bedrock access, Claude models enabled in Bedrock console, and appropriate IAM permissions. +#### Prerequisites -Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure. +AWS account with Bedrock access, Claude models enabled in the Bedrock console, and IAM permissions for `bedrock:InvokeModel*` on the workspace's identity. ```tf -resource "coder_env" "bedrock_use" { - agent_id = coder_agent.main.id - name = "CLAUDE_CODE_USE_BEDROCK" - value = "1" +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.9.3" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + use_bedrock = true + model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" } resource "coder_env" "aws_region" { agent_id = coder_agent.main.id name = "AWS_REGION" - value = "us-east-1" # Choose your preferred region -} - -# Option 1: Using AWS credentials - -variable "aws_access_key_id" { - type = string - description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'." - sensitive = true - value = "xxxx-xxx-xxxx" -} - -variable "aws_secret_access_key" { - type = string - description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console." - sensitive = true - value = "xxxx-xxx-xxxx" -} - -resource "coder_env" "aws_access_key_id" { - agent_id = coder_agent.main.id - name = "AWS_ACCESS_KEY_ID" - value = var.aws_access_key_id -} - -resource "coder_env" "aws_secret_access_key" { - agent_id = coder_agent.main.id - name = "AWS_SECRET_ACCESS_KEY" - value = var.aws_secret_access_key + value = "us-east-1" } +``` -# Option 2: Using Bedrock API key (simpler) +> [!TIP] +> Prefer attaching an IAM role to the workspace (EKS IRSA, EC2 instance profile, or ECS task role) over passing static `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` through Terraform variables. Claude Code picks up the role via the standard AWS credential chain with no additional configuration. -variable "aws_bearer_token_bedrock" { - type = string - description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key." - sensitive = true - value = "xxxx-xxx-xxxx" -} +If your workspaces cannot use an attached IAM role, you can still pass static credentials or a Bedrock bearer token via `coder_env` resources alongside the module: -resource "coder_env" "bedrock_api_key" { +```tf +resource "coder_env" "aws_bearer_token_bedrock" { agent_id = coder_agent.main.id name = "AWS_BEARER_TOKEN_BEDROCK" value = var.aws_bearer_token_bedrock } - -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" -} ``` -> [!NOTE] -> For additional Bedrock configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Bedrock documentation](https://docs.claude.com/en/docs/claude-code/amazon-bedrock). +See the [Claude Code Bedrock documentation](https://docs.claude.com/en/docs/claude-code/amazon-bedrock) for region overrides, model selection, and token limit tuning. ### Usage with Google Vertex AI -#### Prerequisites +Set `use_vertex = true` to route Claude Code through Google Vertex AI. The module sets `CLAUDE_CODE_USE_VERTEX=1` and skips Anthropic API key setup; authentication uses Google Application Default Credentials inside the workspace. -GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, and appropriate IAM permissions (Vertex AI User role). +#### Prerequisites -Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform. +A GCP project with the Vertex AI API enabled, Claude models enabled in Model Garden, and the workspace identity granted the `Vertex AI User` role. ```tf -variable "vertex_sa_json" { - type = string - description = "The complete JSON content of your Google Cloud service account key file. Create a service account in the GCP Console under 'IAM & Admin > Service Accounts', then create and download a JSON key. Copy the entire JSON content into this variable." - sensitive = true -} - -resource "coder_env" "vertex_use" { - agent_id = coder_agent.main.id - name = "CLAUDE_CODE_USE_VERTEX" - value = "1" +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.9.3" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + use_vertex = true + model = "claude-sonnet-4@20250514" } resource "coder_env" "vertex_project_id" { @@ -326,52 +287,28 @@ resource "coder_env" "cloud_ml_region" { name = "CLOUD_ML_REGION" value = "global" } +``` -resource "coder_env" "vertex_sa_json" { - agent_id = coder_agent.main.id - name = "VERTEX_SA_JSON" - value = var.vertex_sa_json -} - -resource "coder_env" "google_application_credentials" { - agent_id = coder_agent.main.id - name = "GOOGLE_APPLICATION_CREDENTIALS" - value = "/tmp/gcp-sa.json" -} - -module "claude-code" { - source = "registry.coder.com/coder/claude-code/coder" - version = "4.9.2" - agent_id = coder_agent.main.id - workdir = "/home/coder/project" - model = "claude-sonnet-4@20250514" - - pre_install_script = <<-EOT - #!/bin/bash - # Write the service account JSON to a file - echo "$VERTEX_SA_JSON" > /tmp/gcp-sa.json - - # Install prerequisite packages - sudo apt-get update - sudo apt-get install -y apt-transport-https ca-certificates gnupg curl +> [!TIP] +> Prefer GKE Workload Identity or an attached service account over shipping a service-account JSON key through Terraform. Claude Code picks up Application Default Credentials automatically. If you must use a key file, mount it and set `GOOGLE_APPLICATION_CREDENTIALS` via a `coder_env` resource. - # Add Google Cloud public key - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg +See the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai) for additional configuration options. - # Add Google Cloud SDK repo to apt sources - echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list +### Usage with a custom API gateway - # Update and install the Google Cloud SDK - sudo apt-get update && sudo apt-get install -y google-cloud-cli +Set `anthropic_base_url` to point Claude Code at a self-hosted gateway or proxy that speaks the Anthropic Messages API. The module sets `ANTHROPIC_BASE_URL` and skips its built-in Anthropic authentication setup; provide whatever credentials your gateway requires via `coder_env`. - # Authenticate gcloud with the service account - gcloud auth activate-service-account --key-file=/tmp/gcp-sa.json - EOT +```tf +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.9.3" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + anthropic_base_url = "https://llm-gateway.example.com/anthropic" } ``` -> [!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). +`anthropic_base_url` is mutually exclusive with `enable_aibridge` (which sets `ANTHROPIC_BASE_URL` to the Coder AI Bridge endpoint automatically). ## Troubleshooting @@ -390,7 +327,7 @@ cat ~/.claude-module/post_install.log ``` > [!NOTE] -> To use tasks with Claude Code, you must provide an `anthropic_api_key` or `claude_code_oauth_token`. +> To use tasks with Claude Code, the workspace must be authenticated to a model provider: set `claude_api_key` or `claude_code_oauth_token` for Anthropic's API, `enable_aibridge = true` for Coder AI Bridge, `use_bedrock = true` / `use_vertex = true` for cloud providers, or `anthropic_base_url` for a custom gateway. > The `workdir` variable is required and specifies the directory where Claude Code will run. ## References diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index b01e88327..8cf802554 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -529,4 +529,73 @@ EOF`, expect(claudeConfig).toContain("typescript-language-server"); expect(claudeConfig).toContain("go-language-server"); }); + + test("use-bedrock-no-api-key", async () => { + const { id, coderEnvVars } = await setup({ + moduleVariables: { + use_bedrock: "true", + report_tasks: "false", + }, + }); + await execModuleScript(id, coderEnvVars); + + expect(coderEnvVars["CLAUDE_CODE_USE_BEDROCK"]).toBe("1"); + expect(coderEnvVars["CLAUDE_API_KEY"]).toBeUndefined(); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + expect(installLog).toContain( + "Using Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1)", + ); + expect(installLog).not.toContain( + "Neither claude_api_key nor enable_aibridge is set", + ); + }); + + test("use-vertex-no-api-key", async () => { + const { id, coderEnvVars } = await setup({ + moduleVariables: { + use_vertex: "true", + report_tasks: "false", + }, + }); + await execModuleScript(id, coderEnvVars); + + expect(coderEnvVars["CLAUDE_CODE_USE_VERTEX"]).toBe("1"); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + expect(installLog).toContain( + "Using Google Vertex AI (CLAUDE_CODE_USE_VERTEX=1)", + ); + expect(installLog).not.toContain( + "Neither claude_api_key nor enable_aibridge is set", + ); + }); + + test("anthropic-base-url", async () => { + const baseUrl = "https://llm-gateway.example.com/anthropic"; + const { id, coderEnvVars } = await setup({ + moduleVariables: { + anthropic_base_url: baseUrl, + report_tasks: "false", + }, + }); + await execModuleScript(id, coderEnvVars); + + expect(coderEnvVars["ANTHROPIC_BASE_URL"]).toBe(baseUrl); + + const installLog = await readFileContainer( + id, + "/home/coder/.claude-module/install.log", + ); + expect(installLog).toContain("Using custom ANTHROPIC_BASE_URL"); + expect(installLog).not.toContain( + "Neither claude_api_key nor enable_aibridge is set", + ); + }); }); diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index db234c052..63cf8a3d9 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -267,6 +267,44 @@ variable "enable_aibridge" { } } +variable "anthropic_base_url" { + type = string + description = "Override the Anthropic API base URL (sets ANTHROPIC_BASE_URL). Use for self-hosted LLM gateways or proxies. Mutually exclusive with enable_aibridge." + default = "" + + validation { + condition = !(var.anthropic_base_url != "" && var.enable_aibridge) + error_message = "anthropic_base_url cannot be provided when enable_aibridge is true. AI Bridge sets ANTHROPIC_BASE_URL automatically." + } +} + +variable "use_bedrock" { + type = bool + description = "Run Claude Code against Amazon Bedrock (sets CLAUDE_CODE_USE_BEDROCK=1). Authentication uses the workspace's AWS credentials (IRSA, instance profile, or AWS_* env vars). Mutually exclusive with enable_aibridge and use_vertex." + default = false + + validation { + condition = !(var.use_bedrock && var.enable_aibridge) + error_message = "use_bedrock cannot be combined with enable_aibridge." + } + + validation { + condition = !(var.use_bedrock && var.use_vertex) + error_message = "use_bedrock and use_vertex are mutually exclusive." + } +} + +variable "use_vertex" { + type = bool + description = "Run Claude Code against Google Vertex AI (sets CLAUDE_CODE_USE_VERTEX=1). Authentication uses Application Default Credentials (Workload Identity or GOOGLE_APPLICATION_CREDENTIALS). Mutually exclusive with enable_aibridge and use_bedrock." + default = false + + validation { + condition = !(var.use_vertex && var.enable_aibridge) + error_message = "use_vertex cannot be combined with enable_aibridge." + } +} + variable "enable_state_persistence" { type = bool description = "Enable AgentAPI conversation state persistence across restarts." @@ -316,10 +354,24 @@ resource "coder_env" "anthropic_model" { } resource "coder_env" "anthropic_base_url" { - count = var.enable_aibridge ? 1 : 0 + count = (var.enable_aibridge || var.anthropic_base_url != "") ? 1 : 0 agent_id = var.agent_id name = "ANTHROPIC_BASE_URL" - value = "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" + value = var.enable_aibridge ? "${data.coder_workspace.me.access_url}/api/v2/aibridge/anthropic" : var.anthropic_base_url +} + +resource "coder_env" "use_bedrock" { + count = var.use_bedrock ? 1 : 0 + agent_id = var.agent_id + name = "CLAUDE_CODE_USE_BEDROCK" + value = "1" +} + +resource "coder_env" "use_vertex" { + count = var.use_vertex ? 1 : 0 + agent_id = var.agent_id + name = "CLAUDE_CODE_USE_VERTEX" + value = "1" } locals { @@ -431,6 +483,9 @@ module "agentapi" { ARG_MCP_CONFIG_REMOTE_PATH='${base64encode(jsonencode(var.mcp_config_remote_path))}' \ ARG_ENABLE_AIBRIDGE='${var.enable_aibridge}' \ ARG_PERMISSION_MODE='${var.permission_mode}' \ + ARG_USE_BEDROCK='${var.use_bedrock}' \ + ARG_USE_VERTEX='${var.use_vertex}' \ + ARG_ANTHROPIC_BASE_URL='${var.anthropic_base_url}' \ /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 9c9df50f4..1319795df 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -459,4 +459,134 @@ run "test_api_key_count_with_aibridge_no_override" { condition = length(coder_env.claude_api_key) == 1 error_message = "CLAUDE_API_KEY env should be created when aibridge is enabled, regardless of session_token value" } +} + +run "test_use_bedrock" { + command = plan + + variables { + agent_id = "test-agent-bedrock" + workdir = "/home/coder/test" + use_bedrock = true + } + + assert { + condition = coder_env.use_bedrock[0].name == "CLAUDE_CODE_USE_BEDROCK" + error_message = "CLAUDE_CODE_USE_BEDROCK env var should be created when use_bedrock is true" + } + + assert { + condition = coder_env.use_bedrock[0].value == "1" + error_message = "CLAUDE_CODE_USE_BEDROCK should be set to 1" + } + + assert { + condition = length(coder_env.claude_api_key) == 0 + error_message = "CLAUDE_API_KEY should not be created when use_bedrock is true and no claude_api_key is provided" + } + + assert { + condition = length(coder_env.use_vertex) == 0 + error_message = "CLAUDE_CODE_USE_VERTEX should not be created when use_bedrock is true" + } +} + +run "test_use_vertex" { + command = plan + + variables { + agent_id = "test-agent-vertex" + workdir = "/home/coder/test" + use_vertex = true + } + + assert { + condition = coder_env.use_vertex[0].name == "CLAUDE_CODE_USE_VERTEX" + error_message = "CLAUDE_CODE_USE_VERTEX env var should be created when use_vertex is true" + } + + assert { + condition = coder_env.use_vertex[0].value == "1" + error_message = "CLAUDE_CODE_USE_VERTEX should be set to 1" + } +} + +run "test_anthropic_base_url_explicit" { + command = plan + + variables { + agent_id = "test-agent-baseurl" + workdir = "/home/coder/test" + anthropic_base_url = "https://llm-gateway.example.com/anthropic" + } + + assert { + condition = coder_env.anthropic_base_url[0].value == "https://llm-gateway.example.com/anthropic" + error_message = "ANTHROPIC_BASE_URL should match the explicit anthropic_base_url input" + } + + assert { + condition = length(coder_env.claude_api_key) == 0 + error_message = "CLAUDE_API_KEY should not be created when only anthropic_base_url is provided" + } +} + +run "test_bedrock_aibridge_validation" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + use_bedrock = true + enable_aibridge = true + } + + expect_failures = [ + var.use_bedrock, + ] +} + +run "test_vertex_aibridge_validation" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + use_vertex = true + enable_aibridge = true + } + + expect_failures = [ + var.use_vertex, + ] +} + +run "test_bedrock_vertex_validation" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + use_bedrock = true + use_vertex = true + } + + expect_failures = [ + var.use_bedrock, + ] +} + +run "test_base_url_aibridge_validation" { + command = plan + + variables { + agent_id = "test-agent-validation" + workdir = "/home/coder/test" + anthropic_base_url = "https://gateway.example.com" + enable_aibridge = true + } + + expect_failures = [ + var.anthropic_base_url, + ] } \ No newline at end of file diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index c00773b5e..68668a4ab 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -23,6 +23,9 @@ ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-} ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-} ARG_ENABLE_AIBRIDGE=${ARG_ENABLE_AIBRIDGE:-false} ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} +ARG_USE_BEDROCK=${ARG_USE_BEDROCK:-false} +ARG_USE_VERTEX=${ARG_USE_VERTEX:-false} +ARG_ANTHROPIC_BASE_URL=${ARG_ANTHROPIC_BASE_URL:-} export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" @@ -40,6 +43,9 @@ printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$ARG_MCP_CONFIG_REMOTE_PATH" 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_USE_BEDROCK: %s\n" "$ARG_USE_BEDROCK" +printf "ARG_USE_VERTEX: %s\n" "$ARG_USE_VERTEX" +printf "ARG_ANTHROPIC_BASE_URL: %s\n" "$ARG_ANTHROPIC_BASE_URL" echo "--------------------------------" @@ -180,6 +186,19 @@ function setup_claude_configurations() { function configure_standalone_mode() { echo "Configuring Claude Code for standalone mode..." + if [ "$ARG_USE_BEDROCK" = "true" ]; then + echo "Using Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1); Anthropic API key not required, skipping authentication setup" + return + fi + if [ "$ARG_USE_VERTEX" = "true" ]; then + echo "Using Google Vertex AI (CLAUDE_CODE_USE_VERTEX=1); Anthropic API key not required, skipping authentication setup" + return + fi + if [ -n "$ARG_ANTHROPIC_BASE_URL" ]; then + echo "Using custom ANTHROPIC_BASE_URL ($ARG_ANTHROPIC_BASE_URL); skipping built-in authentication setup" + return + fi + 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