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
15 changes: 11 additions & 4 deletions registry/coder/modules/agent-relay-claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on each build and runs the Claude Code self-hosted runner.
```tf
module "claude_code_runner" {
source = "registry.coder.com/coder/agent-relay-claude-code/coder"
version = "0.2.0"
version = "1.0.0"
agent_id = coder_agent.main.id

# Downloads the Claude Code CLI at start when it is not in the image. Bake
Expand All @@ -42,6 +42,12 @@ when to reap the workspace.

## Requirements

- A Coder deployment that supports workspace build secrets, and an Agent
Relay release that sends the work order as one. The work order reaches the
runner as the `SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET` workspace secret,
never as a build parameter. A relay that still sends it as the
`agent_relay_credential` parameter refuses to serve a template on this
version; pin `0.2.0` until the relay and Coder are upgraded.
- 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 All @@ -52,8 +58,9 @@ when to reap the workspace.

Agent Relay verifies this contract against the template's active version at
startup and refuses to serve a pool that does not satisfy it. Every parameter
renders disabled with a "Set by Agent Relay on dispatch" placeholder; the
credential is masked.
renders disabled with a "Set by Agent Relay on dispatch" placeholder. The
work order is not among them: it is a workspace secret, so it never appears
in the build's parameters or Terraform state.

`agent_relay_client_platform` is attribution rather than contract: the relay
stamps the surface the session was created from (`web_claude_ai`,
Expand Down Expand Up @@ -93,7 +100,7 @@ rather than reporting `working` forever.
| value | meaning |
| ----------------- | -------------------------------------------------------------------------- |
| `pending` | no state recorded yet |
| `idle` | no credential: workspace was created manually |
| `idle` | no work order: workspace was created manually |
| `working` | runner alive, no session picked up |
| `serving` | runner alive, session picked up (best effort, see `serving_log_pattern`) |
| `orphaned` | runner process gone without recording an exit |
Expand Down
46 changes: 16 additions & 30 deletions registry/coder/modules/agent-relay-claude-code/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@
# - agent_relay_client_platform is persistent attribution: the surface
# that created the session. Stamped on every build; relays that
# predate it simply never set it.
# - agent_relay_credential, agent_relay_claude_code_lock_to_account,
# and agent_relay_attempt are ephemeral runner inputs, reset
# between builds. An ephemeral value is still recorded on the build
# that set it, which is how Agent Relay reads the attempt back when
# it reaps a dead workspace.
# - agent_relay_claude_code_lock_to_account and agent_relay_attempt
# are ephemeral runner inputs, reset between builds. An ephemeral
# value is still recorded on the build that set it, which is how
# Agent Relay reads the attempt back when it reaps a dead workspace.
#
# The work-order JWT is not a parameter. Agent Relay sends it as an
# ephemeral workspace secret, which the agent injects as
# SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET. It never reaches the
# provisioner, Terraform state, or the build's parameters, so nothing
# in this module can read it.
#
# The runner's lifecycle is published through the agent_relay_status agent
# metadata item, whose script this module renders. The script starts the
Expand Down Expand Up @@ -155,22 +160,6 @@ data "coder_parameter" "agent_relay_client_platform" {
})
}

data "coder_parameter" "agent_relay_credential" {
name = "agent_relay_credential"
display_name = "Agent Relay credential"
description = "Single-use work order JWT the runner authenticates to Anthropic with. Agent Relay sets this when it dispatches the workspace; a human never fills it in. Ephemeral: it is valid for one session and is not reused on a later build."
type = "string"
ephemeral = true
mutable = true
default = ""
order = 1003
styling = jsonencode({
disabled = true
mask_input = true
placeholder = "Set by Agent Relay on dispatch"
})
}

data "coder_parameter" "agent_relay_claude_code_lock_to_account" {
name = "agent_relay_claude_code_lock_to_account"
display_name = "Claude Code account lock"
Expand Down Expand Up @@ -202,13 +191,10 @@ data "coder_parameter" "agent_relay_attempt" {
}

# Environment variable names are the claude CLI's contract, not
# Agent Relay's; do not rename them here.
resource "coder_env" "runner_environment_secret" {
agent_id = var.agent_id
name = "SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET"
value = data.coder_parameter.agent_relay_credential.value
}

# Agent Relay's; do not rename them here. The CLI also reads
# SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET, which the agent injects from
# the workspace secret Agent Relay sends; a coder_env of the same name
# would be overridden by it.
resource "coder_env" "agent_relay_claude_code_lock_to_account" {
agent_id = var.agent_id
name = "SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT"
Expand Down Expand Up @@ -275,8 +261,8 @@ output "scripts" {
}

output "dispatched" {
description = "Whether this workspace was spawned by Agent Relay (credential set) or manually (empty)."
value = data.coder_parameter.agent_relay_credential.value != ""
description = "Whether this workspace was spawned by Agent Relay (delivery set) or manually (empty)."
value = data.coder_parameter.agent_relay_delivery_id.value != ""
}

output "client_platform" {
Expand Down
21 changes: 2 additions & 19 deletions registry/coder/modules/agent-relay-claude-code/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ run "parameter_contract" {
error_message = "pool parameter name is part of the Agent Relay contract"
}

assert {
condition = data.coder_parameter.agent_relay_credential.name == "agent_relay_credential"
error_message = "credential parameter name is part of the Agent Relay contract"
}

assert {
condition = data.coder_parameter.agent_relay_claude_code_lock_to_account.name == "agent_relay_claude_code_lock_to_account"
error_message = "account lock parameter name is part of the Agent Relay contract"
Expand All @@ -47,14 +42,13 @@ run "parameter_contract" {

# The relay reads these back off a build long after dispatch, so they
# must not be ephemeral; the rest must be, so a manual build never
# inherits a stale credential, account lock, or attempt.
# inherits a stale account lock or attempt.
assert {
condition = alltrue([
data.coder_parameter.agent_relay_session_id.ephemeral == false,
data.coder_parameter.agent_relay_delivery_id.ephemeral == false,
data.coder_parameter.agent_relay_pool.ephemeral == false,
data.coder_parameter.agent_relay_client_platform.ephemeral == false,
data.coder_parameter.agent_relay_credential.ephemeral == true,
data.coder_parameter.agent_relay_claude_code_lock_to_account.ephemeral == true,
data.coder_parameter.agent_relay_attempt.ephemeral == true,
])
Expand All @@ -70,29 +64,18 @@ run "parameter_contract" {
data.coder_parameter.agent_relay_delivery_id.styling,
data.coder_parameter.agent_relay_pool.styling,
data.coder_parameter.agent_relay_client_platform.styling,
data.coder_parameter.agent_relay_credential.styling,
data.coder_parameter.agent_relay_claude_code_lock_to_account.styling,
data.coder_parameter.agent_relay_attempt.styling,
] : can(regex("\"disabled\":true", p))
])
error_message = "every relay parameter must render disabled"
}

assert {
condition = can(regex("\"mask_input\":true", data.coder_parameter.agent_relay_credential.styling))
error_message = "the credential must be masked"
}
}

run "runner_wiring" {
command = plan

# The claude CLI owns these names; the module only supplies values.
assert {
condition = coder_env.runner_environment_secret.name == "SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET"
error_message = "the pool secret env var name is the claude CLI's contract"
}

assert {
condition = coder_env.agent_relay_claude_code_lock_to_account.name == "SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT"
error_message = "the account lock env var name is the claude CLI's contract"
Expand Down Expand Up @@ -151,7 +134,7 @@ run "runner_wiring" {

assert {
condition = output.dispatched == false
error_message = "a build with no credential was not dispatched by Agent Relay"
error_message = "a build with no delivery was not dispatched by Agent Relay"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if [ -z "$${SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET:-}" ]; then
write_state "idle"
fi
echo "No work order set. This workspace was created manually, not by Agent Relay."
echo "Idling. Delete this workspace manually or set the agent_relay_credential parameter."
echo "Idling. Delete this workspace manually; only Agent Relay can deliver a work order."
exit 0
fi

Expand Down
18 changes: 11 additions & 7 deletions registry/coder/modules/agent-relay-cursor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ parameters the relay stamps on each build and runs the Cursor CLI worker.
```tf
module "cursor_worker" {
source = "registry.coder.com/coder/agent-relay-cursor/coder"
version = "0.5.0"
version = "1.0.0"
agent_id = coder_agent.main.id

# Downloads the Cursor CLI at start when it is not in the image. Bake
Expand Down Expand Up @@ -52,6 +52,10 @@ created.

## Requirements

- A Coder deployment that supports workspace build secrets, and an Agent
Relay release that sends the worker token as one. A relay that still sends
it as the `agent_relay_credential` parameter refuses to serve a template on
this version; pin `0.5.0` until the relay and Coder are upgraded.
- The Cursor CLI (`agent`) 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 All @@ -71,9 +75,10 @@ created.

The pool's service-account API key never leaves Agent Relay. At dispatch the
relay exchanges it for a Cursor sub-token scoped to the requesting user and
stamps that as the ephemeral `agent_relay_credential` parameter. The module
exports it as `AGENT_RELAY_CURSOR_TOKEN` and starts the worker with
`--auth-token`. It is never written to disk, though as a command-line argument
sends that as an ephemeral workspace secret, which the agent injects as
`AGENT_RELAY_CURSOR_TOKEN`. It never appears in the build's parameters or
Terraform state. The module starts the worker with `--auth-token`. It is never
written to disk, though as a command-line argument
it is visible in the worker's `/proc/<pid>/cmdline` to any process running as
the same user inside the workspace; the CLI offers no environment variable for
it. The token acts only as that user, cannot mint further tokens, and expires
Expand All @@ -84,8 +89,7 @@ fails and Cursor re-queues the request for a fresh workspace.

Agent Relay verifies this contract against the template's active version at
startup and refuses to serve a pool that does not satisfy it. Every parameter
renders disabled with a "Set by Agent Relay on dispatch" placeholder; the
credential is masked.
renders disabled with a "Set by Agent Relay on dispatch" placeholder.

## Scripts and logs

Expand Down Expand Up @@ -115,7 +119,7 @@ finishes a turn, not when the chat closes, so keep the timeout at or above
| value | meaning |
| ----------------- | -------------------------------------------------------------------------- |
| `pending` | start step running, supervisor has not written yet |
| `idle` | no credential: workspace was created manually |
| `idle` | no worker token: workspace was created manually |
| `working` | worker alive, no session attached |
| `serving` | worker alive, session attached (best effort, see `serving_log_pattern`) |
| `orphaned` | worker process gone without recording an exit |
Expand Down
43 changes: 13 additions & 30 deletions registry/coder/modules/agent-relay-cursor/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
# dedupe and reconciliation query workspaces with `param:` search
# filters on them. agent_relay_cursor_repo_url is stamped on every
# build (empty for repo-less pools) so the contract stays static.
# - agent_relay_credential is an ephemeral worker input, reset between
# builds.
#
# The worker token is not a parameter. Agent Relay sends it as an
# ephemeral workspace secret, which the agent injects as
# AGENT_RELAY_CURSOR_TOKEN. It never reaches the provisioner, Terraform
# state, or the build's parameters, so nothing in this module can read
# it.
#
# The worker's lifecycle is published through the agent_relay_status agent
# metadata item, whose script this module renders. The script starts
Expand Down Expand Up @@ -188,33 +192,12 @@ data "coder_parameter" "agent_relay_cursor_repo_url" {
})
}

data "coder_parameter" "agent_relay_credential" {
name = "agent_relay_credential"
display_name = "Agent Relay credential"
description = "Short-lived Cursor worker token, scoped to the user who requested the agent, that the worker authenticates with. Agent Relay mints it from the pool's service account key when it dispatches the workspace; a human never fills it in. Ephemeral: it expires an hour after minting and is not reused on a later build."
type = "string"
ephemeral = true
mutable = true
default = ""
order = 1006
styling = jsonencode({
disabled = true
mask_input = true
placeholder = "Set by Agent Relay on dispatch"
})
}

# CURSOR_AGENT_WORKER_ID is the Cursor CLI's contract; do not rename it.
# The worker token has no CLI env var (only the --auth-token flag), so it
# travels under an Agent Relay name and the script passes it on the
# command line. It is never CURSOR_API_KEY: the CLI would treat the
# token as a service account key and reject it.
resource "coder_env" "agent_relay_cursor_token" {
agent_id = var.agent_id
name = "AGENT_RELAY_CURSOR_TOKEN"
value = data.coder_parameter.agent_relay_credential.value
}

# The worker token has no CLI env var (only the --auth-token flag), so
# Agent Relay delivers it as the AGENT_RELAY_CURSOR_TOKEN workspace
# secret and the script passes it on the command line. It is never
# CURSOR_API_KEY: the CLI would treat the token as a service account key
# and reject it.
resource "coder_env" "cursor_agent_worker_id" {
agent_id = var.agent_id
name = "CURSOR_AGENT_WORKER_ID"
Expand Down Expand Up @@ -290,8 +273,8 @@ output "scripts" {
}

output "dispatched" {
description = "Whether this workspace was spawned by Agent Relay (credential set) or manually (empty)."
value = data.coder_parameter.agent_relay_credential.value != ""
description = "Whether this workspace was spawned by Agent Relay (delivery set) or manually (empty)."
value = data.coder_parameter.agent_relay_delivery_id.value != ""
}

output "session_id" {
Expand Down
27 changes: 5 additions & 22 deletions registry/coder/modules/agent-relay-cursor/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ run "parameter_contract" {
error_message = "pool parameter name is part of the Agent Relay contract"
}

assert {
condition = data.coder_parameter.agent_relay_credential.name == "agent_relay_credential"
error_message = "credential parameter name is part of the Agent Relay contract"
}

assert {
condition = data.coder_parameter.agent_relay_cursor_pool_name.name == "agent_relay_cursor_pool_name"
error_message = "Cursor pool parameter name is part of the Agent Relay contract"
Expand All @@ -45,8 +40,8 @@ run "parameter_contract" {
error_message = "Cursor idle release timeout parameter name is part of the Agent Relay contract"
}

# Only the credential is ephemeral: the rest are queried back off the
# workspace with `param:` filters, which only sees declared values.
# None are ephemeral: they are queried back off the workspace with
# `param:` filters, which only sees declared values.
assert {
condition = alltrue([
data.coder_parameter.agent_relay_session_id.ephemeral == false,
Expand All @@ -55,7 +50,6 @@ run "parameter_contract" {
data.coder_parameter.agent_relay_cursor_pool_name.ephemeral == false,
data.coder_parameter.agent_relay_cursor_repo_url.ephemeral == false,
data.coder_parameter.agent_relay_cursor_idle_release_timeout.ephemeral == false,
data.coder_parameter.agent_relay_credential.ephemeral == true,
])
error_message = "parameter persistence does not match the Agent Relay contract"
}
Expand All @@ -71,28 +65,17 @@ run "parameter_contract" {
data.coder_parameter.agent_relay_cursor_pool_name.styling,
data.coder_parameter.agent_relay_cursor_repo_url.styling,
data.coder_parameter.agent_relay_cursor_idle_release_timeout.styling,
data.coder_parameter.agent_relay_credential.styling,
] : can(regex("\"disabled\":true", p))
])
error_message = "every relay parameter must render disabled"
}

assert {
condition = can(regex("\"mask_input\":true", data.coder_parameter.agent_relay_credential.styling))
error_message = "the credential must be masked"
}
}

run "worker_wiring" {
command = plan

# The Cursor CLI owns the worker id name; the token has no CLI env var
# and must not masquerade as CURSOR_API_KEY.
assert {
condition = coder_env.agent_relay_cursor_token.name == "AGENT_RELAY_CURSOR_TOKEN"
error_message = "the worker token env var is Agent Relay's, not a Cursor API key"
}

# The Cursor CLI owns the worker id name. The token arrives as a
# workspace secret, not through this module.
assert {
condition = coder_env.cursor_agent_worker_id.name == "CURSOR_AGENT_WORKER_ID"
error_message = "the worker id env var name is the Cursor CLI's contract"
Expand Down Expand Up @@ -170,7 +153,7 @@ run "worker_wiring" {

assert {
condition = output.dispatched == false
error_message = "a build with no credential was not dispatched by Agent Relay"
error_message = "a build with no delivery was not dispatched by Agent Relay"
}

assert {
Expand Down
2 changes: 1 addition & 1 deletion registry/coder/modules/agent-relay-cursor/start.sh.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ if [ -z "$${AGENT_RELAY_CURSOR_TOKEN:-}" ]; then
write_state "idle"
fi
echo "No worker token set. This workspace was created manually, not by Agent Relay."
echo "Idling. Delete this workspace manually or set the agent_relay_credential parameter."
echo "Idling. Delete this workspace manually; only Agent Relay can deliver a worker token."
exit 0
fi

Expand Down
Loading