diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 3d875046f..c5bd265f5 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.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -55,15 +55,43 @@ module "claude-code" { This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access. -By default, when `enable_boundary = true`, the module uses `coder boundary` subcommand (provided by Coder) without requiring any installation. +When `enable_boundary = true`, you must provide network filtering rules via one of two options: + +- `boundary_config` — inline YAML string (config lives in the template) +- `boundary_config_path` — path to a config file already on disk + +The module writes the config to `~/.config/coder_boundary/config.yaml` automatically. + +#### Inline boundary config ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true + + boundary_config = <<-EOT + allow: + - "*.anthropic.com" + - "*.github.com" + EOT +} +``` + +#### Boundary config from file path + +Use this when the config file is provisioned separately or managed outside the template: + +```tf +module "claude-code" { + source = "registry.coder.com/coder/claude-code/coder" + version = "4.9.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_boundary = true + boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml" } ``` @@ -81,7 +109,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.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_aibridge = true @@ -110,7 +138,7 @@ data "coder_task" "me" {} module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" ai_prompt = data.coder_task.me.prompt @@ -133,7 +161,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.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -189,7 +217,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.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -211,7 +239,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -284,7 +312,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.9.0" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -341,7 +369,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.8.0" + version = "4.9.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 337ebd201..9d972dc89 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -225,6 +225,38 @@ variable "enable_boundary" { type = bool description = "Whether to enable coder boundary for network filtering" default = false + + validation { + condition = !var.enable_boundary || var.boundary_config == null || var.boundary_config_path == null + error_message = "Only one of boundary_config or boundary_config_path can be provided, not both." + } + + validation { + condition = (var.boundary_config == null && var.boundary_config_path == null) || var.enable_boundary + error_message = "boundary_config and boundary_config_path can only be set when enable_boundary is true." + } +} + +variable "boundary_config" { + type = string + description = "Inline YAML config for coder boundary network filtering rules. Written to ~/.config/coder_boundary/config.yaml before boundary starts. Mutually exclusive with boundary_config_path." + default = null + + validation { + condition = var.boundary_config == null || trimspace(var.boundary_config) != "" + error_message = "boundary_config must not be empty or whitespace-only when provided." + } +} + +variable "boundary_config_path" { + type = string + description = "Path to an existing boundary config file on disk. Symlinked to ~/.config/coder_boundary/config.yaml before boundary starts. Mutually exclusive with boundary_config." + default = null + + validation { + condition = var.boundary_config_path == null || trimspace(var.boundary_config_path) != "" + error_message = "boundary_config_path must not be empty or whitespace-only when provided." + } } variable "boundary_version" { @@ -325,8 +357,9 @@ locals { start_script = file("${path.module}/scripts/start.sh") module_dir_name = ".claude-module" # Extract hostname from access_url for boundary --allow flag - coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") - claude_api_key = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key + coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") + boundary_config_b64 = var.boundary_config != null ? base64encode(var.boundary_config) : "" + claude_api_key = var.enable_aibridge ? data.coder_workspace_owner.me.session_token : var.claude_api_key # Required prompts for the module to properly report task status to Coder report_tasks_system_prompt = <<-EOT @@ -400,6 +433,8 @@ module "agentapi" { ARG_COMPILE_FROM_SOURCE='${var.compile_boundary_from_source}' \ ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \ ARG_CODER_HOST='${local.coder_host}' \ + ARG_BOUNDARY_CONFIG='${local.boundary_config_b64}' \ + ARG_BOUNDARY_CONFIG_PATH='${var.boundary_config_path != null ? var.boundary_config_path : ""}' \ ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \ /tmp/start.sh EOT diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 3d11989bc..bd51bb0c2 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -188,13 +188,61 @@ run "test_claude_code_permission_mode_validation" { } } -run "test_claude_code_with_boundary" { +run "test_claude_code_with_boundary_inline_config" { command = plan variables { agent_id = "test-agent-boundary" workdir = "/home/coder/boundary-test" enable_boundary = true + boundary_config = <<-EOT + allow: + - "*.anthropic.com" + - "*.github.com" + EOT + } + + override_data { + target = data.coder_workspace.me + values = { + access_url = "https://coder.example.com" + } + } + + assert { + condition = var.enable_boundary == true + error_message = "Boundary should be enabled" + } + + assert { + condition = var.boundary_config != null + error_message = "Boundary config should be set" + } + + assert { + condition = local.coder_host == "coder.example.com" + error_message = "Coder host should be 'coder.example.com' after stripping https:// from access URL" + } + + assert { + condition = local.boundary_config_b64 != "" + error_message = "Boundary config should be base64-encoded for the start script" + } + + assert { + condition = base64decode(local.boundary_config_b64) == var.boundary_config + error_message = "Base64-encoded boundary config should decode back to the original config" + } +} + +run "test_claude_code_with_boundary_config_path" { + command = plan + + variables { + agent_id = "test-agent-boundary-path" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml" } assert { @@ -203,9 +251,130 @@ run "test_claude_code_with_boundary" { } assert { - condition = local.coder_host != "" - error_message = "Coder host should be extracted from access URL" + condition = var.boundary_config_path == "/home/coder/.config/coder_boundary/config.yaml" + error_message = "Boundary config path should be set correctly" + } +} + +run "test_claude_code_with_boundary_no_config" { + command = plan + + variables { + agent_id = "test-agent-boundary" + workdir = "/home/coder/boundary-test" + enable_boundary = true + } + + assert { + condition = var.enable_boundary == true + error_message = "Boundary should be enabled" + } +} + +run "test_boundary_both_configs_fails" { + command = plan + + variables { + agent_id = "test-agent-boundary-both" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_config = "allow:\n - '*.example.com'" + boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml" + } + + expect_failures = [ + var.enable_boundary, + ] +} + +run "test_boundary_config_without_boundary_fails" { + command = plan + + variables { + agent_id = "test-agent-no-boundary" + workdir = "/home/coder/boundary-test" + enable_boundary = false + boundary_config = "allow:\n - '*.example.com'" + } + + expect_failures = [ + var.enable_boundary, + ] +} + +run "test_boundary_config_path_without_boundary_fails" { + command = plan + + variables { + agent_id = "test-agent-no-boundary-path" + workdir = "/home/coder/boundary-test" + enable_boundary = false + boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml" + } + + expect_failures = [ + var.enable_boundary, + ] +} + +run "test_boundary_empty_config_fails" { + command = plan + + variables { + agent_id = "test-agent-empty-config" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_config = "" } + + expect_failures = [ + var.boundary_config, + ] +} + +run "test_boundary_empty_config_path_fails" { + command = plan + + variables { + agent_id = "test-agent-empty-config-path" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_config_path = "" + } + + expect_failures = [ + var.boundary_config_path, + ] +} + +run "test_boundary_whitespace_config_fails" { + command = plan + + variables { + agent_id = "test-agent-whitespace-config" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_config = " " + } + + expect_failures = [ + var.boundary_config, + ] +} + +run "test_boundary_whitespace_config_path_fails" { + command = plan + + variables { + agent_id = "test-agent-whitespace-config-path" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_config_path = " " + } + + expect_failures = [ + var.boundary_config_path, + ] } run "test_claude_code_system_prompt" { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 2df8fce12..d1efdf62e 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -24,6 +24,10 @@ ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"latest"} ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false} ARG_USE_BOUNDARY_DIRECTLY=${ARG_USE_BOUNDARY_DIRECTLY:-false} ARG_CODER_HOST=${ARG_CODER_HOST:-} +ARG_BOUNDARY_CONFIG=${ARG_BOUNDARY_CONFIG:-} +ARG_BOUNDARY_CONFIG_PATH=${ARG_BOUNDARY_CONFIG_PATH:-} +ARG_BOUNDARY_CONFIG_PATH="${ARG_BOUNDARY_CONFIG_PATH/#\~/$HOME}" +ARG_BOUNDARY_CONFIG_PATH="${ARG_BOUNDARY_CONFIG_PATH//\$HOME/$HOME}" echo "--------------------------------" @@ -223,6 +227,29 @@ function start_agentapi() { printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" if [ "$ARG_ENABLE_BOUNDARY" = "true" ]; then + BOUNDARY_CONFIG_DIR="$HOME/.config/coder_boundary" + BOUNDARY_CONFIG_FILE="$BOUNDARY_CONFIG_DIR/config.yaml" + + if [ -n "$ARG_BOUNDARY_CONFIG" ]; then + printf "Writing inline boundary config to %s\n" "$BOUNDARY_CONFIG_FILE" + mkdir -p "$BOUNDARY_CONFIG_DIR" + echo -n "$ARG_BOUNDARY_CONFIG" | base64 -d > "$BOUNDARY_CONFIG_FILE" + if [ ! -s "$BOUNDARY_CONFIG_FILE" ]; then + printf "Error: boundary configuration file '%s' does not exist or is empty after writing inline config.\n" "$BOUNDARY_CONFIG_FILE" >&2 + exit 1 + fi + elif [ -n "$ARG_BOUNDARY_CONFIG_PATH" ]; then + printf "Linking boundary config from %s to %s\n" "$ARG_BOUNDARY_CONFIG_PATH" "$BOUNDARY_CONFIG_FILE" + if [ "$ARG_BOUNDARY_CONFIG_PATH" != "$BOUNDARY_CONFIG_FILE" ]; then + mkdir -p "$BOUNDARY_CONFIG_DIR" + ln -sf "$ARG_BOUNDARY_CONFIG_PATH" "$BOUNDARY_CONFIG_FILE" + fi + if [ ! -s "$BOUNDARY_CONFIG_FILE" ]; then + printf "Error: boundary configuration file '%s' does not exist or is empty. Check that '%s' exists and is not empty.\n" "$BOUNDARY_CONFIG_FILE" "$ARG_BOUNDARY_CONFIG_PATH" >&2 + exit 1 + fi + fi + install_boundary printf "Starting with coder boundary enabled\n"