diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index ea845f6e..76f656dc 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -55,7 +55,14 @@ 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" { @@ -64,6 +71,27 @@ module "claude-code" { 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.8.0" + agent_id = coder_agent.main.id + workdir = "/home/coder/project" + enable_boundary = true + boundary_config_path = "/home/coder/.config/coder_boundary/config.yaml" } ``` diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 7a08aa82..c35fd05d 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -231,6 +231,33 @@ 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 = "When enable_boundary is true, at least one of boundary_config or boundary_config_path must be provided." + } + + 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 +} + +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 } variable "boundary_version" { @@ -407,6 +434,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='${var.boundary_config != null ? base64encode(var.boundary_config) : ""}' \ + 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 66c79bab..9646c5da 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -188,13 +188,18 @@ 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 } assert { @@ -202,12 +207,98 @@ run "test_claude_code_with_boundary" { error_message = "Boundary should be enabled" } + assert { + condition = var.boundary_config != null + error_message = "Boundary config should be set" + } + assert { condition = local.coder_host != "" error_message = "Coder host should be extracted from access URL" } } +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 { + condition = var.enable_boundary == true + error_message = "Boundary should be enabled" + } + + assert { + condition = var.boundary_config_path == "/home/coder/.config/coder_boundary/config.yaml" + error_message = "Boundary config path should be set correctly" + } +} + +run "test_boundary_without_config_fails" { + command = plan + + variables { + agent_id = "test-agent-boundary-fail" + workdir = "/home/coder/boundary-test" + enable_boundary = true + } + + expect_failures = [ + var.enable_boundary, + ] +} + +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_claude_code_system_prompt" { command = plan diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 5ccbc8fa..0567772e 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -24,6 +24,8 @@ 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:-} echo "--------------------------------" @@ -223,6 +225,21 @@ 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" + 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 + fi + install_boundary printf "Starting with coder boundary enabled\n"