diff --git a/registry/coder/modules/agent-relay-cursor/README.md b/registry/coder/modules/agent-relay-cursor/README.md index 7ea7331dd..29fefc442 100644 --- a/registry/coder/modules/agent-relay-cursor/README.md +++ b/registry/coder/modules/agent-relay-cursor/README.md @@ -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 @@ -69,10 +69,10 @@ 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//cmdline` to any process running as the same user inside the workspace; the CLI offers no environment variable for @@ -80,22 +80,6 @@ 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 diff --git a/registry/coder/modules/agent-relay-cursor/main.test.ts b/registry/coder/modules/agent-relay-cursor/main.test.ts index 29bdcf565..395892d1d 100644 --- a/registry/coder/modules/agent-relay-cursor/main.test.ts +++ b/registry/coder/modules/agent-relay-cursor/main.test.ts @@ -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 = {}) => { @@ -366,15 +365,14 @@ 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, @@ -382,21 +380,8 @@ describe("agent-relay-cursor", () => { ]); 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 () => { diff --git a/registry/coder/modules/agent-relay-cursor/main.tf b/registry/coder/modules/agent-relay-cursor/main.tf index 2b654e373..b3b72d2ba 100644 --- a/registry/coder/modules/agent-relay-cursor/main.tf +++ b/registry/coder/modules/agent-relay-cursor/main.tf @@ -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 @@ -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" @@ -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 @@ -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" @@ -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 diff --git a/registry/coder/modules/agent-relay-cursor/main.tftest.hcl b/registry/coder/modules/agent-relay-cursor/main.tftest.hcl index 1bd9097aa..4b175a0f6 100644 --- a/registry/coder/modules/agent-relay-cursor/main.tftest.hcl +++ b/registry/coder/modules/agent-relay-cursor/main.tftest.hcl @@ -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" @@ -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, diff --git a/registry/coder/modules/agent-relay-cursor/start.sh.tftpl b/registry/coder/modules/agent-relay-cursor/start.sh.tftpl index bfaf5fc1f..a0bb3be24 100644 --- a/registry/coder/modules/agent-relay-cursor/start.sh.tftpl +++ b/registry/coder/modules/agent-relay-cursor/start.sh.tftpl @@ -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 ~}