Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions registry/coder/modules/agent-relay-claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <noreply@anthropic.com>` 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
Expand Down
28 changes: 28 additions & 0 deletions registry/coder/modules/agent-relay-claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ variable "client_label" {
default = ""
}

variable "configure_git" {
description = <<-EOT
Set the global git identity to Claude <noreply@anthropic.com> 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"
Expand Down Expand Up @@ -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"
Expand All @@ -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)
Expand Down
63 changes: 63 additions & 0 deletions registry/coder/modules/agent-relay-claude-code/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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"
}
}
6 changes: 6 additions & 0 deletions registry/coder/modules/agent-relay-claude-code/start.sh.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down
Loading