diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 4836347b7..fd3df5ec1 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -192,6 +192,30 @@ variable "claude_md_path" { default = "$HOME/.claude/CLAUDE.md" } +variable "enable_boundary" { + type = bool + description = "Whether to enable coder boundary for network filtering" + default = false +} + +variable "boundary_log_dir" { + type = string + description = "Directory for boundary logs" + default = "/tmp/boundary_logs" +} + +variable "boundary_unprivileged" { + type = bool + description = "Whether to use --unprivileged flag with coder boundary (recommended for security)" + default = true +} + +variable "boundary_additional_allowed_urls" { + type = list(string) + description = "Additional URLs to allow through boundary (in addition to default allowed URLs)" + default = [] +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 @@ -222,6 +246,12 @@ resource "coder_env" "claude_api_key" { value = var.claude_api_key } +resource "coder_env" "mcp_server_port" { + agent_id = var.agent_id + name = "MCP_SERVER_PORT" + value = "8081" +} + locals { # we have to trim the slash because otherwise coder exp mcp will # set up an invalid claude config @@ -231,6 +261,8 @@ locals { start_script = file("${path.module}/scripts/start.sh") module_dir_name = ".claude-module" remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh")) + # Extract hostname from access_url for boundary --allow flag + coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "") } module "agentapi" { @@ -270,6 +302,11 @@ module "agentapi" { ARG_PERMISSION_MODE='${var.permission_mode}' \ ARG_WORKDIR='${local.workdir}' \ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ + ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \ + ARG_BOUNDARY_UNPRIVILEGED='${var.boundary_unprivileged}' \ + ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \ + ARG_CODER_HOST='${local.coder_host}' \ /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 c48923cf3..55eedd5e6 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -187,3 +187,29 @@ run "test_claude_code_permission_mode_validation" { error_message = "Permission mode should be one of the valid options" } } + +run "test_claude_code_with_boundary" { + command = plan + + variables { + agent_id = "test-agent-boundary" + workdir = "/home/coder/boundary-test" + enable_boundary = true + boundary_log_dir = "/tmp/test-boundary-logs" + } + + assert { + condition = var.enable_boundary == true + error_message = "Boundary should be enabled" + } + + assert { + condition = var.boundary_log_dir == "/tmp/test-boundary-logs" + error_message = "Boundary log dir should be set correctly" + } + + assert { + condition = local.coder_host != "" + error_message = "Coder host should be extracted from access URL" + } +} diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index b5fca7a5a..195730ccf 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -15,6 +15,10 @@ ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) +ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} +ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"} +ARG_BOUNDARY_UNPRIVILEGED=${ARG_BOUNDARY_UNPRIVILEGED:-true} +ARG_CODER_HOST=${ARG_CODER_HOST:-} echo "--------------------------------" @@ -25,6 +29,10 @@ printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIO printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE" printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT" printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" +printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR" +printf "ARG_BOUNDARY_UNPRIVILEGED: %s\n" "$ARG_BOUNDARY_UNPRIVILEGED" +printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" echo "--------------------------------" @@ -74,7 +82,33 @@ function start_agentapi() { fi fi printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" - agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" + + if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then + mkdir -p "$ARG_BOUNDARY_LOG_DIR" + printf "Starting with coder boundary enabled\n" + + # Build boundary args with conditional --unprivileged flag + BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR") + if [ "${ARG_BOUNDARY_UNPRIVILEGED:-true}" = "true" ]; then + BOUNDARY_ARGS+=(--unprivileged) + fi + # Add default allowed URLs + BOUNDARY_ARGS+=(--allow "*.anthropic.com" --allow "registry.npmjs.org" --allow "*.sentry.io" --allow "claude.ai" --allow "$ARG_CODER_HOST") + + # Add any additional allowed URLs from the variable + if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then + IFS=' ' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" + for url in "${ADDITIONAL_URLS[@]}"; do + BOUNDARY_ARGS+=(--allow "$url") + done + fi + + agentapi server --type claude --term-width 67 --term-height 1190 -- \ + coder exp boundary "${BOUNDARY_ARGS[@]}" -- \ + claude "${ARGS[@]}" + else + agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" + fi } validate_claude_installation