diff --git a/registry/coder/modules/agent-relay-claude-code/README.md b/registry/coder/modules/agent-relay-claude-code/README.md index 4fe13edb5..1d02ba818 100644 --- a/registry/coder/modules/agent-relay-claude-code/README.md +++ b/registry/coder/modules/agent-relay-claude-code/README.md @@ -49,6 +49,8 @@ when to reap the workspace. ## Requirements +- A Coder deployment running 2.37 or newer, which Agent Relay refuses to + start against otherwise. - The `claude` CLI must be in the workspace. `install_cli` (default `true`) downloads it at start only when it is not already on PATH; bake it into the image for the fastest start. `cli_binary` overrides the path. @@ -146,6 +148,48 @@ rather than reporting `working` forever. Renaming the `agent_relay_status` key breaks reaping. +## Git authentication + +`use_anthropic_git_proxy` authenticates clones server-side, so the +workspace holds no git credentials. A session a person created uses their +GitHub OAuth token; one a bot or agent created uses the organization's +GitHub App installation token. + +```tf +module "claude_code_runner" { + # ... + use_anthropic_git_proxy = true +} +``` + +That second path is the only git auth that works when the workspace owner +is a **service account**, which you detect with `login_type = "none"`. +Such an owner can never sign in, so it can never complete the OAuth flow +a `coder_external_auth` block depends on. Gate on it if you want the +proxy only there; the flag serves human sessions equally well, so +enabling it everywhere is also fine: + +```tf +# login_type "none" means a service account: it cannot sign in, so it +# cannot link GitHub. +use_anthropic_git_proxy = data.coder_workspace_owner.me.login_type == "none" +``` + +`configure_git` follows it by default, setting the identity to +`Claude ` with signed commits. The proxy replaces +the HOME git config with credentials only, so without this there is no +identity and commits fail. Override either way: + +```tf +configure_git = false # image keeps its identity in /etc/gitconfig +configure_git = true # sign as Claude without the proxy +``` + +**The proxy rewrites the runner account's git config**, deleting +`~/.gitconfig`, the `GIT_CONFIG_GLOBAL` target and all of +`$XDG_CONFIG_HOME/git` at startup and before every session, with no +backup. Do not enable it on a workspace a person also works in. + ## Graceful shutdown The start step detaches the supervisor with `setsid`, so it lives in its own diff --git a/registry/coder/modules/agent-relay-claude-code/main.tf b/registry/coder/modules/agent-relay-claude-code/main.tf index 6138640db..3e13d0639 100644 --- a/registry/coder/modules/agent-relay-claude-code/main.tf +++ b/registry/coder/modules/agent-relay-claude-code/main.tf @@ -113,6 +113,28 @@ variable "client_label" { default = "" } +variable "configure_git" { + description = <<-EOT + Set the global git identity to Claude and sign commits through Anthropic's signing service (the CLI's --configure-git). + + Defaults to following use_anthropic_git_proxy: that flag replaces the HOME git config with credentials only, so the proxy alone leaves no identity and commits fail. Set false if your image keeps an identity in /etc/gitconfig, which the proxy does not touch; set true to sign commits without the proxy. + EOT + type = bool + default = null +} + +variable "use_anthropic_git_proxy" { + description = <<-EOT + Authenticate clones server-side instead of holding git credentials in the workspace (the CLI's --use-anthropic-git-proxy). A session a person created uses their GitHub OAuth token; one a bot or agent created uses the organization's GitHub App installation token. + + That second path is the only one that works when the workspace owner is a service account, which login_type "none" indicates: it can never sign in, so it can never complete a coder_external_auth flow. + + DESTRUCTIVE: at startup and before every session the runner deletes ~/.gitconfig, the GIT_CONFIG_GLOBAL target and all of $XDG_CONFIG_HOME/git, with no backup. That isolation is the point for an ephemeral single-session workspace. Do not enable it on a workspace a person also works in, and expect it to override any identity a git-config module wrote. + EOT + type = bool + default = false +} + variable "serving_log_pattern" { type = string default = "Picked up session" @@ -231,6 +253,10 @@ locals { # tree so one directory holds everything a debugger needs. module_directory = "$HOME/.coder-modules/coder/agent-relay-claude-code" + # The proxy restores credentials but no identity, so the two travel + # together unless a template says otherwise. + configure_git = var.configure_git != null ? var.configure_git : var.use_anthropic_git_proxy + # coder-utils prefixes its own steps with this; the stop step is a # plain coder_script, so it joins the same naming by hand. display_name_prefix = "Claude Code runner" @@ -251,6 +277,8 @@ locals { exit_if_unused_min = var.exit_if_unused_min drain_wait_sec = var.drain_wait_sec push_outcome_on_release = var.push_outcome_on_release + use_anthropic_git_proxy = var.use_anthropic_git_proxy + configure_git = local.configure_git # Free-form text: base64 so a label with quotes or spaces can never # become shell in the supervisor script. client_label = base64encode(local.client_label) diff --git a/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl b/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl index 62637b9e9..a9eb4d799 100644 --- a/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl +++ b/registry/coder/modules/agent-relay-claude-code/main.tftest.hcl @@ -321,6 +321,13 @@ run "graceful_shutdown_defaults" { condition = !strcontains(local.start_script, "--push-outcome-on-release") && !strcontains(local.start_script, "--drain-wait-sec") error_message = "both optional runner behaviours are off by default" } + + # It rewrites the account's HOME git config before every session, so it + # is never on unless the template asked for it. + assert { + condition = !strcontains(local.start_script, "--use-anthropic-git-proxy") && !strcontains(local.start_script, "--configure-git") + error_message = "Anthropic-managed git and the Claude git identity must both be opt-in" + } } run "drain_wait_enabled" { @@ -423,3 +430,59 @@ run "stop_script_waits_for_the_recorded_exit" { error_message = "the stop script must wait for the supervisor to record the exit, not just for the runner to go" } } + +run "anthropic_git_proxy_enabled" { + command = plan + + variables { + use_anthropic_git_proxy = true + } + + assert { + condition = strcontains(local.start_script, "--use-anthropic-git-proxy") + error_message = "use_anthropic_git_proxy must reach the runner as a flag" + } + + # The proxy leaves no identity behind, so the two travel together + # unless a template says otherwise. + assert { + condition = strcontains(local.start_script, "--configure-git") + error_message = "the Claude git identity must follow the proxy by default" + } + + # It authenticates clones server-side; it does not change what the + # runner has to do on the way out. + assert { + condition = output.shutdown_grace_seconds == 105 + error_message = "Anthropic-managed git must not change the shutdown budget" + } +} + +run "git_identity_overrides_the_proxy_default" { + command = plan + + variables { + use_anthropic_git_proxy = true + configure_git = false + } + + # For an image that supplies its identity in /etc/gitconfig, which the + # proxy does not delete. + assert { + condition = strcontains(local.start_script, "--use-anthropic-git-proxy") && !strcontains(local.start_script, "--configure-git") + error_message = "configure_git = false must win over the proxy default" + } +} + +run "git_identity_without_the_proxy" { + command = plan + + variables { + configure_git = true + } + + assert { + condition = strcontains(local.start_script, "--configure-git") && !strcontains(local.start_script, "--use-anthropic-git-proxy") + error_message = "signing commits must not require Anthropic-managed git" + } +} diff --git a/registry/coder/modules/agent-relay-claude-code/start.sh.tftpl b/registry/coder/modules/agent-relay-claude-code/start.sh.tftpl index 13dda0558..e2a35ad2c 100644 --- a/registry/coder/modules/agent-relay-claude-code/start.sh.tftpl +++ b/registry/coder/modules/agent-relay-claude-code/start.sh.tftpl @@ -104,6 +104,12 @@ ${cli_binary} self-hosted-runner \\ %{ endif ~} %{ if push_outcome_on_release ~} --push-outcome-on-release \\ +%{ endif ~} +%{ if use_anthropic_git_proxy ~} + --use-anthropic-git-proxy \\ +%{ endif ~} +%{ if configure_git ~} + --configure-git \\ %{ endif ~} --client-label "\$client_label" \\ --exec-path "$wrapper" >"$log_file" 2>&1 &