Skip to content

feat(agent-relay-cursor): drain the worker on workspace stop - #1145

Draft
matifali wants to merge 1 commit into
mainfrom
matifali/agent-relay-cursor-graceful-shutdown
Draft

matifali wants to merge 1 commit into
mainfrom
matifali/agent-relay-cursor-graceful-shutdown

Conversation

@matifali

Copy link
Copy Markdown
Member

The same gap as #1143, in the same place, for the Cursor module. Independent of it — different files, no conflict.

Problem

start.sh.tftpl detaches the supervisor with setsid, so it lives in its own session and never receives the SIGTERM the container's init gets. The worker is killed outright: it never deregisters from its pool, its sessionEnd hook never runs, and work in flight is lost.

What is different from the Claude module

I checked the Cursor CLI's surface rather than assuming it mirrors Claude's, and it does not:

Claude runner Cursor worker
setsid detachment gap yes yes
state format working <pid> / done <code> identical
--drain-wait-sec, --retire-at, --push-outcome-on-release yes none exist
advertises a shutdown budget 80s, printed at startup none, prints nothing

Verified against agent worker --help on the CLI in ghcr.io/coder/agent-relay-workspace (build 2026.09.23-86fc751).

So shutdown_grace_seconds here is an input with a default of 60, not a value computed from the CLI as in #1143. There is nothing to derive it from, and the README says so rather than implying a number that came from somewhere.

What is shared

The stop script is the same shape: SIGTERM only, never escalates, never writes the state file, always exits 0, and stays out of the coder exp sync chain.

One improvement over #1143, taken from this module: it reuses the existing worker_alive helper, which confirms the pid is still the worker via /proc/<pid>/cmdline before signalling. A bare kill -0 would trust a recycled pid, and the stop step is the one caller that actually sends a signal. I have backported that guard to #1143.

Not included

agent worker has several flags this module does not expose, which I looked at and deliberately left out of a shutdown PR:

  • --name — display name, defaults to hostname. The analogue of client_label.
  • --label key=value — repeatable, unlike Claude's single label. Whether it affects routing is undocumented, so I make no claim.
  • --mint-github-token — short-lived GitHub tokens during claimed runs, pool workers only, requires a team admin to enable. The analogue of the Anthropic git proxy in feat(agent-relay-claude-code): support Anthropic-managed git #1144, and the same answer for a service-account owner that cannot complete coder_external_auth.
  • --management-addr — /healthz, /readyz and a Prometheus /metrics scrape.

Each is worth its own PR.

Testing

terraform test: 13 pass, including the pid guard, SIGTERM-only, the budget matching between the output and the script, an override, and rejection of 0. bun test: 18 pass.

Not verified: whether the Cursor worker traps SIGTERM internally and drains, or just dies. That needs a valid pool credential — the worker exits immediately on a bad key. I tried reading the binary for signal handling and it is packed, so strings returns nothing even for known flag names; that check proves nothing either way. The stop script is correct regardless, since without it the worker gets no signal at all.

Minor bump, 0.2.0 → 0.3.0.

Generated with Claude Code using Claude Opus 4.5.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Module Scorecard Check

coder/agent-relay-cursor: 56 → 59

✅ Score improvement: 56 → 59 (+3).

Theme Before After
Presentation & Onboarding 6 / 17 6 / 17
Integration 10 / 25 5 / 25
Credential Hygiene 12 / 20 20 / 20
Restricted-Environment 13.5 / 20 13.5 / 20
Engineering Quality 10 / 10 10 / 10
Overall 56 / 100 59 / 100
Full scorecard for this PR
Presentation & Onboarding Agent Integration Credential Hygiene Restricted-Environment Readiness Engineering Quality Overall
6 / 17 5 / 25 20 / 20 13.5 / 20 10 / 10 59 / 100
Drilldown

Presentation & Onboarding — 6 / 17

Criterion Max Score Notes
Configuration-mode examples 12 half One well-annotated primary example (module "cursor_worker" with agent_id, install_cli) plus a second docker_container.destroy_grace_seconds snippet. There are no distinct "modes" (e.g. computer_use = true, the insecure_shared_token/api_key credential path, Kubernetes grace-period wiring) shown as worked code examples — only prose.
Visual preview 5 0 No image, GIF, or video anywhere in the README.

Agent Integration — 5 / 25

Criterion Max Score Notes
AI governance 10 0 README documents "Agent Relay" dispatch/credential exchange but never mentions Coder AI Gateway or Agent Firewall, which are the specific governance surfaces the criterion requires.
Dashboard entry point 5 full coder_app.cursor_web and coder_app.cursor_desktop are declared and documented in the README ("Open in Cursor Web" / "Open in Cursor Desktop"), gated correctly on agent_relay_session_id.
Session continuity 5 0 README explicitly states the opposite of continuity: "It is not refreshed: a worker that has to reconnect after expiry fails and Cursor re-queues the request for a fresh workspace." No resume/session-manager support documented.
Managed configuration 5 0 No documented MCP, settings, or policy management. agent_relay_cursor_repo_url is a contract parameter cloned by the template, not configuration the module manages.

Credential Hygiene — 20 / 20

Criterion Max Score Notes
Secrets marked sensitive 16 full agent_relay_credential is ephemeral = true and rendered with mask_input = true styling; the "Worker credential" README section documents exactly how the token flows (env var only, never written to disk, expires in 1h, visible only via /proc/<pid>/cmdline). No inline secrets appear in any README example.
Non-hardcoded auth path 4 full README documents a token-exchange flow: the service-account key never leaves Agent Relay, which mints a short-lived per-user sub-token exported as AGENT_RELAY_CURSOR_TOKEN — a documented non-hardcoded auth path.

Restricted-Environment Readiness — 13.5 / 20

Criterion Max Score Notes
Mirrorable artifact source 5 0 install.sh.tftpl hardcodes `curl https://cursor.com/install -fsSL
Bring-your-own binary 10 full install_cli = false fully skips the download, and cli_binary plus the documented ~/.local/bin lookup let a baked-in CLI be used; README explicitly recommends this ("Bake the CLI into the image and set this to false for faster workspaces").
Egress transparency 3 half No dedicated network/egress/air-gapped section. Endpoints (cursor.com/install, cursor.com/agents/<id>, cursor://... deeplink) are only mentioned scattered across the install-flow and app descriptions, capping this at half per rubric.
Runs without sudo 2 full None of install.sh.tftpl, start.sh.tftpl, stop.sh.tftpl, or status.sh.tftpl invoke sudo; all run as the workspace user.

Engineering Quality — 10 / 10

Criterion Max Score Notes
Input quality 6 full Every variable has a clear description and sensible default; cli_binary, shutdown_grace_seconds, and serving_log_pattern all carry validation blocks guarding against shell injection or degenerate values.
Test coverage 4 full main.tftest.hcl thoroughly covers parameter contract, credential-kind branching, injection safety, and script wiring; main.test.ts runs true end-to-end container tests exercising install/start/stop lifecycle, restart handling, and shutdown draining.

Overall — 59 / 100

Raw 54.5 / 92 → round(54.5 / 92 × 100) = 59

Tip

You can run this locally by telling your agent: "review this module against .github/scorecard/SCORECARD.md".


Scored against SCORECARD.md with solstice-1. Language-model scores are advisory.

@matifali

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T13:03:44.024923Z dd44219 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd4421987b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

install_cli = var.install_cli
})

stop_script = templatefile("${path.module}/stop.sh.tftpl", {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Place the stop template under scripts/

The new template is loaded from the module root, but the repository convention requires script templates to live under scripts/ and be rendered at plan time. Move it to scripts/stop.sh.tftpl and update this templatefile() path so module tooling and maintainers can rely on the required layout.

AGENTS.md reference: AGENTS.md:L56-L69

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping this one, and I want to be explicit about why rather than silently ignore it.

The rule is real, but this module's three existing templates — install.sh.tftpl, start.sh.tftpl, status.sh.tftpl — all live at the module root, as do the Claude module's. Moving only the new file would leave one script in scripts/ and three beside it, which is worse for a maintainer than either consistent layout.

Happy to move all four (in both modules) as its own PR, where the diff is a pure relocation and easy to review. Doing it here would mix a layout change into a shutdown fix.

Comment on lines +85 to +87
type = number
default = 60
description = <<-EOT

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Put the variable description before its type

This new variable block places type and default before description, contrary to the repository's required description → type → default → validation → sensitive ordering. Reorder these attributes to keep generated module source consistent with the documented convention.

AGENTS.md reference: AGENTS.md:L113-L124

Useful? React with 👍 / 👎.

Comment on lines +22 to +23
[ -n "$${1:-}" ] && kill -0 "$1" 2>/dev/null &&
tr '\0' ' ' <"/proc/$1/cmdline" 2>/dev/null | grep -qF -- "$(basename "${cli_binary}")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the worker command before sending SIGTERM

When a stale working <pid> entry points to a recycled PID, this substring test accepts any command line containing the generic basename agent—including a Coder process invoked with an agent argument—and line 47 then sends SIGTERM to that unrelated process. This defeats the PID-reuse protection precisely in the recovery case it is meant to handle; verify the executable or the expected agent worker ... argv structure rather than grepping for the basename anywhere.

Useful? React with 👍 / 👎.

@matifali
matifali force-pushed the matifali/agent-relay-cursor-graceful-shutdown branch from dd44219 to 2dbb057 Compare September 26, 2026 04:37
The start step detaches the supervisor with setsid, so it lives in its
own session and never receives the SIGTERM the container's init gets on
shutdown. The worker was therefore killed outright: it never
deregistered from its pool, its sessionEnd hook never ran, and work in
flight was lost. This is the same gap the Claude Code module has, in the
same place.

Add a run_on_stop script that relays the signal and waits for the
supervisor to record the exit -- not merely for the pid to disappear,
which can happen before the supervisor resumes from `wait` and writes
"done <code>", leaving a state file that reads as orphaned.

Tighten the module's worker_alive guard while reusing it. Matching the
bare basename accepted any command line containing "agent", including
the workspace agent's own, which defeats the pid-reuse protection
exactly in the recovery case it exists for; it now matches the argv the
supervisor launches. The stop step is the one caller that signals, so
the copies in start and status move with it.

Export shutdown_grace_seconds for the template to wire into
docker_container.destroy_grace_seconds or a pod's
termination_grace_period_seconds, since a module owns no compute
resource.

Unlike the Claude Code runner, this one is an input rather than a
computed value. The Cursor CLI advertises no shutdown budget and exposes
no drain, retire or push-outcome flag -- verified against
`agent worker --help` -- so there is nothing to derive it from. It
defaults to 60 seconds and is the operator's to tune.
@matifali
matifali force-pushed the matifali/agent-relay-cursor-graceful-shutdown branch from 2dbb057 to 19d7579 Compare September 26, 2026 04:54
@matifali matifali added the version:minor Add to PRs requiring a minor version upgrade label Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

version:minor Add to PRs requiring a minor version upgrade

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant