Skip to content

Commit 98c6040

Browse files
feat(coder/agent-relay-cursor): remove the shared service-account credential path (#1169)
Co-authored-by: Coder Prototypes <294797144+coder-prototypes@users.noreply.github.com>
1 parent f21e23b commit 98c6040

5 files changed

Lines changed: 21 additions & 102 deletions

File tree

‎registry/coder/modules/agent-relay-cursor/README.md‎

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ parameters the relay stamps on each build and runs the Cursor CLI worker.
1616
```tf
1717
module "cursor_worker" {
1818
source = "registry.coder.com/coder/agent-relay-cursor/coder"
19-
version = "0.4.0"
19+
version = "0.5.0"
2020
agent_id = coder_agent.main.id
2121
2222
# Downloads the Cursor CLI at start when it is not in the image. Bake
@@ -69,33 +69,17 @@ created.
6969

7070
## Worker credential
7171

72-
By default the pool's service-account API key never leaves Agent Relay. At
73-
dispatch the relay exchanges it for a Cursor sub-token scoped to the requesting
74-
user and stamps that as the ephemeral `agent_relay_credential` parameter. It
75-
is exported as `AGENT_RELAY_CURSOR_TOKEN` and the worker starts with
72+
The pool's service-account API key never leaves Agent Relay. At dispatch the
73+
relay exchanges it for a Cursor sub-token scoped to the requesting user and
74+
stamps that as the ephemeral `agent_relay_credential` parameter. The module
75+
exports it as `AGENT_RELAY_CURSOR_TOKEN` and starts the worker with
7676
`--auth-token`. It is never written to disk, though as a command-line argument
7777
it is visible in the worker's `/proc/<pid>/cmdline` to any process running as
7878
the same user inside the workspace; the CLI offers no environment variable for
7979
it. The token acts only as that user, cannot mint further tokens, and expires
8080
after an hour. It is not refreshed: a worker that has to reconnect after expiry
8181
fails and Cursor re-queues the request for a fresh workspace.
8282

83-
Some Cursor CLI releases refuse a delegated sub-token for pool workers
84-
(`Delegated service-account tokens cannot start pool workers`). Agent Relay's
85-
per-pool `insecure_shared_token` then stamps the service-account key itself,
86-
together with `agent_relay_cursor_credential_kind = api_key`. The CLI accepts
87-
that key only as an API key, never as `--auth-token`
88-
(`Failed to validate worker account settings`), so the supervisor exports the
89-
credential as `CURSOR_API_KEY` and drops `--auth-token` when the stamped kind
90-
says so. Every workspace owner in such a pool can read a team-wide key from
91-
the worker's environment. No template change is needed; the relay stamps the
92-
kind and the module follows it.
93-
94-
| pool `insecure_shared_token` | stamped `agent_relay_cursor_credential_kind` | credential handed to the CLI as |
95-
| ---------------------------- | -------------------------------------------- | ------------------------------- |
96-
| `false` (default) | `worker_token` | `--auth-token` |
97-
| `true` | `api_key` | `CURSOR_API_KEY` |
98-
9983
## Parameters
10084

10185
Agent Relay verifies this contract against the template's active version at

‎registry/coder/modules/agent-relay-cursor/main.test.ts‎

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ const DISPATCH_ENV = [
5353
"CURSOR_AGENT_WORKER_ID=worker-123",
5454
`AGENT_RELAY_CURSOR_POOL_NAME=${POOL_NAME}`,
5555
"AGENT_RELAY_CURSOR_IDLE_RELEASE_TIMEOUT=600",
56-
"AGENT_RELAY_CURSOR_CREDENTIAL_KIND=worker_token",
5756
];
5857

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

369-
it("hands a shared service-account key over as CURSOR_API_KEY", async () => {
370-
// insecure_shared_token pools stamp the team key and
371-
// agent_relay_cursor_credential_kind=api_key; the CLI only accepts
372-
// that key as an API key, so --auth-token must be absent and the key
373-
// must reach the worker's environment.
368+
it("passes the credential as --auth-token even with a stale credential kind", async () => {
369+
// Module 0.2.0 to 0.4.0 exported AGENT_RELAY_CURSOR_CREDENTIAL_KIND
370+
// and switched to CURSOR_API_KEY on api_key. A leftover value in the
371+
// environment must not change how the credential reaches the CLI.
374372
const { id, scripts } = await setup();
375373
await stubAgent(
376374
id,
377-
'printf "%s\\n" "$@" >/tmp/agent-args; printf "%s" "$CURSOR_API_KEY" >/tmp/agent-api-key; sleep 30',
375+
'printf "%s\\n" "$@" >/tmp/agent-args; printf "%s" "${CURSOR_API_KEY-unset}" >/tmp/agent-api-key; sleep 30',
378376
);
379377
const { start } = await runScripts(id, scripts, [
380378
...DISPATCH_ENV,
381379
"AGENT_RELAY_CURSOR_CREDENTIAL_KIND=api_key",
382380
]);
383381
expect(start.exitCode).toBe(0);
384382
const args = (await readFileContainer(id, "/tmp/agent-args")).split("\n");
385-
expect(args).not.toContain("--auth-token");
386-
expect(args).toContain("--pool");
387-
expect(await readFileContainer(id, "/tmp/agent-api-key")).toBe(
388-
"test-user-token",
389-
);
390-
// The key is still read from the environment at run time, never
391-
// written into the supervisor.
392-
const supervisor = await readFileContainer(
393-
id,
394-
`${MODULE_DIR}/scripts/supervise.sh`,
395-
);
396-
expect(supervisor).toContain(
397-
'export CURSOR_API_KEY="$AGENT_RELAY_CURSOR_TOKEN"',
398-
);
399-
expect(supervisor).not.toContain("test-user-token");
383+
expect(args[args.indexOf("--auth-token") + 1]).toBe("test-user-token");
384+
expect(await readFileContainer(id, "/tmp/agent-api-key")).toBe("unset");
400385
});
401386

402387
it("passes --computer-use when enabled", async () => {

‎registry/coder/modules/agent-relay-cursor/main.tf‎

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
#
1515
# - agent_relay_session_id, agent_relay_delivery_id, agent_relay_pool,
1616
# agent_relay_cursor_pool_name, agent_relay_cursor_idle_release_timeout,
17-
# agent_relay_cursor_repo_url, and agent_relay_cursor_credential_kind
18-
# are persistent state: coderd only
17+
# and agent_relay_cursor_repo_url are persistent state: coderd only
1918
# stores parameter values the template declares, and Agent Relay's
2019
# dedupe and reconciliation query workspaces with `param:` search
2120
# filters on them. agent_relay_cursor_repo_url is stamped on every
@@ -189,29 +188,6 @@ data "coder_parameter" "agent_relay_cursor_repo_url" {
189188
})
190189
}
191190

192-
data "coder_parameter" "agent_relay_cursor_credential_kind" {
193-
name = "agent_relay_cursor_credential_kind"
194-
display_name = "Cursor credential kind"
195-
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."
196-
type = "string"
197-
mutable = true
198-
default = "worker_token"
199-
order = 1006
200-
styling = jsonencode({
201-
disabled = true
202-
placeholder = "Set by Agent Relay on dispatch"
203-
})
204-
205-
option {
206-
name = "Worker token (--auth-token)"
207-
value = "worker_token"
208-
}
209-
option {
210-
name = "Service-account API key (CURSOR_API_KEY)"
211-
value = "api_key"
212-
}
213-
}
214-
215191
data "coder_parameter" "agent_relay_credential" {
216192
name = "agent_relay_credential"
217193
display_name = "Agent Relay credential"
@@ -220,7 +196,7 @@ data "coder_parameter" "agent_relay_credential" {
220196
ephemeral = true
221197
mutable = true
222198
default = ""
223-
order = 1007
199+
order = 1006
224200
styling = jsonencode({
225201
disabled = true
226202
mask_input = true
@@ -229,12 +205,10 @@ data "coder_parameter" "agent_relay_credential" {
229205
}
230206

231207
# CURSOR_AGENT_WORKER_ID is the Cursor CLI's contract; do not rename it.
232-
# The credential travels under an Agent Relay name and the supervisor
233-
# decides how to hand it to the CLI from the stamped credential kind: a
234-
# per-user worker token has no CLI env var and goes on the command line
235-
# as --auth-token; a team service-account key is only accepted as
236-
# CURSOR_API_KEY. Each is rejected through the other door, which is why
237-
# the relay stamps the kind alongside the credential.
208+
# The worker token has no CLI env var (only the --auth-token flag), so it
209+
# travels under an Agent Relay name and the script passes it on the
210+
# command line. It is never CURSOR_API_KEY: the CLI would treat the
211+
# token as a service account key and reject it.
238212
resource "coder_env" "agent_relay_cursor_token" {
239213
agent_id = var.agent_id
240214
name = "AGENT_RELAY_CURSOR_TOKEN"
@@ -262,12 +236,6 @@ resource "coder_env" "agent_relay_cursor_idle_release_timeout" {
262236
value = data.coder_parameter.agent_relay_cursor_idle_release_timeout.value
263237
}
264238

265-
resource "coder_env" "agent_relay_cursor_credential_kind" {
266-
agent_id = var.agent_id
267-
name = "AGENT_RELAY_CURSOR_CREDENTIAL_KIND"
268-
value = data.coder_parameter.agent_relay_cursor_credential_kind.value
269-
}
270-
271239
locals {
272240
# coder-utils requires this exact layout. Scripts land in scripts/ and
273241
# their output in logs/; the worker state and log default to the same

‎registry/coder/modules/agent-relay-cursor/main.tftest.hcl‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ run "parameter_contract" {
5555
data.coder_parameter.agent_relay_cursor_pool_name.ephemeral == false,
5656
data.coder_parameter.agent_relay_cursor_repo_url.ephemeral == false,
5757
data.coder_parameter.agent_relay_cursor_idle_release_timeout.ephemeral == false,
58-
data.coder_parameter.agent_relay_cursor_credential_kind.ephemeral == false,
5958
data.coder_parameter.agent_relay_credential.ephemeral == true,
6059
])
6160
error_message = "parameter persistence does not match the Agent Relay contract"
@@ -146,14 +145,8 @@ run "worker_wiring" {
146145
# environment; it must not be expanded into the supervisor file the
147146
# script writes to disk.
148147
assert {
149-
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\"")
150-
error_message = "the supervisor must offer both credential paths, each reading the token from the environment at run time"
151-
}
152-
153-
# The relay stamps which path applies; the supervisor branches on it.
154-
assert {
155-
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"
156-
error_message = "the credential kind is a stamped parameter defaulting to worker_token, exported for the supervisor"
148+
condition = strcontains(local.start_script, "--auth-token \"\\$AGENT_RELAY_CURSOR_TOKEN\"")
149+
error_message = "the worker must authenticate with --auth-token read from the environment at run time"
157150
}
158151

159152
# Reaping reads this file through the agent_relay_status metadata item,

‎registry/coder/modules/agent-relay-cursor/start.sh.tftpl‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,10 @@ write_state() {
108108
}
109109
110110
export PATH="\$HOME/.local/bin:\$PATH"
111-
# The relay stamps how the credential must reach the CLI. A pool with
112-
# insecure_shared_token sends its team service-account key, which the
113-
# CLI accepts only as CURSOR_API_KEY; a per-user worker token is only
114-
# accepted as --auth-token. Both are read from the environment at run
115-
# time so neither value lands in this file.
116-
auth_args=()
117-
if [ "\$${AGENT_RELAY_CURSOR_CREDENTIAL_KIND:-worker_token}" = api_key ]; then
118-
export CURSOR_API_KEY="\$AGENT_RELAY_CURSOR_TOKEN"
119-
else
120-
auth_args=(--auth-token "\$AGENT_RELAY_CURSOR_TOKEN")
121-
fi
122111
${cli_binary} worker \\
123112
--pool "\$AGENT_RELAY_CURSOR_POOL_NAME" \\
124113
--idle-release-timeout "\$AGENT_RELAY_CURSOR_IDLE_RELEASE_TIMEOUT" \\
125-
\$${auth_args[@]+"\$${auth_args[@]}"} \\
114+
--auth-token "\$AGENT_RELAY_CURSOR_TOKEN" \\
126115
%{ if computer_use ~}
127116
--computer-use \\
128117
%{ endif ~}

0 commit comments

Comments
 (0)