Skip to content
Merged
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
26 changes: 5 additions & 21 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.4.0"
version = "0.5.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 @@ -69,33 +69,17 @@ created.

## Worker credential

By default 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. It
is exported as `AGENT_RELAY_CURSOR_TOKEN` and the worker starts with
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
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
after an hour. It is not refreshed: a worker that has to reconnect after expiry
fails and Cursor re-queues the request for a fresh workspace.

Some Cursor CLI releases refuse a delegated sub-token for pool workers
(`Delegated service-account tokens cannot start pool workers`). Agent Relay's
per-pool `insecure_shared_token` then stamps the service-account key itself,
together with `agent_relay_cursor_credential_kind = api_key`. The CLI accepts
that key only as an API key, never as `--auth-token`
(`Failed to validate worker account settings`), so the supervisor exports the
credential as `CURSOR_API_KEY` and drops `--auth-token` when the stamped kind
says so. Every workspace owner in such a pool can read a team-wide key from
the worker's environment. No template change is needed; the relay stamps the
kind and the module follows it.

| pool `insecure_shared_token` | stamped `agent_relay_cursor_credential_kind` | credential handed to the CLI as |
| ---------------------------- | -------------------------------------------- | ------------------------------- |
| `false` (default) | `worker_token` | `--auth-token` |
| `true` | `api_key` | `CURSOR_API_KEY` |

## Parameters

Agent Relay verifies this contract against the template's active version at
Expand Down
29 changes: 7 additions & 22 deletions registry/coder/modules/agent-relay-cursor/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ const DISPATCH_ENV = [
"CURSOR_AGENT_WORKER_ID=worker-123",
`AGENT_RELAY_CURSOR_POOL_NAME=${POOL_NAME}`,
"AGENT_RELAY_CURSOR_IDLE_RELEASE_TIMEOUT=600",
"AGENT_RELAY_CURSOR_CREDENTIAL_KIND=worker_token",
];

const setup = async (vars: Record<string, string> = {}) => {
Expand Down Expand Up @@ -366,37 +365,23 @@ describe("agent-relay-cursor", () => {
expect(env.stdout).not.toContain("CURSOR_API_KEY=");
});

it("hands a shared service-account key over as CURSOR_API_KEY", async () => {
// insecure_shared_token pools stamp the team key and
// agent_relay_cursor_credential_kind=api_key; the CLI only accepts
// that key as an API key, so --auth-token must be absent and the key
// must reach the worker's environment.
it("passes the credential as --auth-token even with a stale credential kind", async () => {
// Module 0.2.0 to 0.4.0 exported AGENT_RELAY_CURSOR_CREDENTIAL_KIND
// and switched to CURSOR_API_KEY on api_key. A leftover value in the
// environment must not change how the credential reaches the CLI.
const { id, scripts } = await setup();
await stubAgent(
id,
'printf "%s\\n" "$@" >/tmp/agent-args; printf "%s" "$CURSOR_API_KEY" >/tmp/agent-api-key; sleep 30',
'printf "%s\\n" "$@" >/tmp/agent-args; printf "%s" "${CURSOR_API_KEY-unset}" >/tmp/agent-api-key; sleep 30',
);
const { start } = await runScripts(id, scripts, [
...DISPATCH_ENV,
"AGENT_RELAY_CURSOR_CREDENTIAL_KIND=api_key",
]);
expect(start.exitCode).toBe(0);
const args = (await readFileContainer(id, "/tmp/agent-args")).split("\n");
expect(args).not.toContain("--auth-token");
expect(args).toContain("--pool");
expect(await readFileContainer(id, "/tmp/agent-api-key")).toBe(
"test-user-token",
);
// The key is still read from the environment at run time, never
// written into the supervisor.
const supervisor = await readFileContainer(
id,
`${MODULE_DIR}/scripts/supervise.sh`,
);
expect(supervisor).toContain(
'export CURSOR_API_KEY="$AGENT_RELAY_CURSOR_TOKEN"',
);
expect(supervisor).not.toContain("test-user-token");
expect(args[args.indexOf("--auth-token") + 1]).toBe("test-user-token");
expect(await readFileContainer(id, "/tmp/agent-api-key")).toBe("unset");
});

it("passes --computer-use when enabled", async () => {
Expand Down
44 changes: 6 additions & 38 deletions registry/coder/modules/agent-relay-cursor/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
#
# - agent_relay_session_id, agent_relay_delivery_id, agent_relay_pool,
# agent_relay_cursor_pool_name, agent_relay_cursor_idle_release_timeout,
# agent_relay_cursor_repo_url, and agent_relay_cursor_credential_kind
# are persistent state: coderd only
# and agent_relay_cursor_repo_url are persistent state: coderd only
# stores parameter values the template declares, and Agent Relay's
# dedupe and reconciliation query workspaces with `param:` search
# filters on them. agent_relay_cursor_repo_url is stamped on every
Expand Down Expand Up @@ -189,29 +188,6 @@ data "coder_parameter" "agent_relay_cursor_repo_url" {
})
}

data "coder_parameter" "agent_relay_cursor_credential_kind" {
name = "agent_relay_cursor_credential_kind"
display_name = "Cursor credential kind"
description = "How the worker hands agent_relay_credential to the Cursor CLI. worker_token passes the per-user sub-token as --auth-token. api_key exports the pool's service-account key as CURSOR_API_KEY, which is the only form the CLI accepts it in; Agent Relay sets this to api_key for pools with insecure_shared_token. A human never fills it in."
type = "string"
mutable = true
default = "worker_token"
order = 1006
styling = jsonencode({
disabled = true
placeholder = "Set by Agent Relay on dispatch"
})

option {
name = "Worker token (--auth-token)"
value = "worker_token"
}
option {
name = "Service-account API key (CURSOR_API_KEY)"
value = "api_key"
}
}

data "coder_parameter" "agent_relay_credential" {
name = "agent_relay_credential"
display_name = "Agent Relay credential"
Expand All @@ -220,7 +196,7 @@ data "coder_parameter" "agent_relay_credential" {
ephemeral = true
mutable = true
default = ""
order = 1007
order = 1006
styling = jsonencode({
disabled = true
mask_input = true
Expand All @@ -229,12 +205,10 @@ data "coder_parameter" "agent_relay_credential" {
}

# CURSOR_AGENT_WORKER_ID is the Cursor CLI's contract; do not rename it.
# The credential travels under an Agent Relay name and the supervisor
# decides how to hand it to the CLI from the stamped credential kind: a
# per-user worker token has no CLI env var and goes on the command line
# as --auth-token; a team service-account key is only accepted as
# CURSOR_API_KEY. Each is rejected through the other door, which is why
# the relay stamps the kind alongside the credential.
# 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"
Expand Down Expand Up @@ -262,12 +236,6 @@ resource "coder_env" "agent_relay_cursor_idle_release_timeout" {
value = data.coder_parameter.agent_relay_cursor_idle_release_timeout.value
}

resource "coder_env" "agent_relay_cursor_credential_kind" {
agent_id = var.agent_id
name = "AGENT_RELAY_CURSOR_CREDENTIAL_KIND"
value = data.coder_parameter.agent_relay_cursor_credential_kind.value
}

locals {
# coder-utils requires this exact layout. Scripts land in scripts/ and
# their output in logs/; the worker state and log default to the same
Expand Down
11 changes: 2 additions & 9 deletions registry/coder/modules/agent-relay-cursor/main.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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_cursor_credential_kind.ephemeral == false,
data.coder_parameter.agent_relay_credential.ephemeral == true,
])
error_message = "parameter persistence does not match the Agent Relay contract"
Expand Down Expand Up @@ -146,14 +145,8 @@ run "worker_wiring" {
# environment; it must not be expanded into the supervisor file the
# script writes to disk.
assert {
condition = strcontains(local.start_script, "auth_args=(--auth-token \"\\$AGENT_RELAY_CURSOR_TOKEN\")") && strcontains(local.start_script, "export CURSOR_API_KEY=\"\\$AGENT_RELAY_CURSOR_TOKEN\"")
error_message = "the supervisor must offer both credential paths, each reading the token from the environment at run time"
}

# The relay stamps which path applies; the supervisor branches on it.
assert {
condition = data.coder_parameter.agent_relay_cursor_credential_kind.name == "agent_relay_cursor_credential_kind" && data.coder_parameter.agent_relay_cursor_credential_kind.default == "worker_token" && coder_env.agent_relay_cursor_credential_kind.name == "AGENT_RELAY_CURSOR_CREDENTIAL_KIND"
error_message = "the credential kind is a stamped parameter defaulting to worker_token, exported for the supervisor"
condition = strcontains(local.start_script, "--auth-token \"\\$AGENT_RELAY_CURSOR_TOKEN\"")
error_message = "the worker must authenticate with --auth-token read from the environment at run time"
}

# Reaping reads this file through the agent_relay_status metadata item,
Expand Down
13 changes: 1 addition & 12 deletions registry/coder/modules/agent-relay-cursor/start.sh.tftpl
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,10 @@ write_state() {
}

export PATH="\$HOME/.local/bin:\$PATH"
# The relay stamps how the credential must reach the CLI. A pool with
# insecure_shared_token sends its team service-account key, which the
# CLI accepts only as CURSOR_API_KEY; a per-user worker token is only
# accepted as --auth-token. Both are read from the environment at run
# time so neither value lands in this file.
auth_args=()
if [ "\$${AGENT_RELAY_CURSOR_CREDENTIAL_KIND:-worker_token}" = api_key ]; then
export CURSOR_API_KEY="\$AGENT_RELAY_CURSOR_TOKEN"
else
auth_args=(--auth-token "\$AGENT_RELAY_CURSOR_TOKEN")
fi
${cli_binary} worker \\
--pool "\$AGENT_RELAY_CURSOR_POOL_NAME" \\
--idle-release-timeout "\$AGENT_RELAY_CURSOR_IDLE_RELEASE_TIMEOUT" \\
\$${auth_args[@]+"\$${auth_args[@]}"} \\
--auth-token "\$AGENT_RELAY_CURSOR_TOKEN" \\
%{ if computer_use ~}
--computer-use \\
%{ endif ~}
Expand Down
Loading