feat(bin): run crew launches under each harness's own controls and gate cursor on unattended work - #3507
Conversation
Launch Claude with --permission-mode auto, Codex with -s workspace-write -a never, and Cursor with --auto-review --sandbox enabled, instead of the three full-bypass flags (--dangerously-skip-permissions, --dangerously-bypass-approvals-and-sandbox, --yolo). A crewmate now runs under its harness's own approval, review, and sandbox controls rather than with them switched off. Codex's workspace-write confines writes to the task worktree plus /tmp and $TMPDIR. That is the intended blast radius, but it also denies the supervision paths a crewmate's contract needs outside its worktree. Granting those back narrowly is a separate, independently reviewable change and is deliberately not bundled here. Refreshes the launch expectations in the three suites that assert the composed command, and the Cursor adapter reference rows. Other tracked uses of the bypass flags are one-off CLI invocations in guard docs and live probes, not spawn output, and are left untouched.
…mpting launch, pin claude auto mode
Supersedes an aborted fix round that would have made an auto-detected cursor silently fall back to another adapter. Silently substituting a different tool changes which adapter runs the captain's work without saying so, which is harder to notice than a refusal. The bar now holds however cursor was selected, explicit or inherited, and the refusal names the exact remedy: set config/crew-harness to a verified adapter, or add an eligible crew-dispatch profile. Firstmate does not pick one on the captain's behalf. The two exemption grants are no longer interchangeable across relaunch. envelope:<name> describes a mechanically proven outer isolation envelope that still governs the replacement agent, so it is inherited and automatic recovery keeps working. `attended` asserts a person is in the pane right now; the captain who attested may have left long before stuck-worker recovery relaunches, so inheriting it would let one attestation authorize unlimited unattended launches. It is never inherited. A cursor exemption is refused outright on a non-cursor harness rather than recorded, so no stale grant can sit in a task's metadata waiting to authorize a later relaunch onto cursor. Remote secondmate launches forward only an envelope grant, through self-describing arguments whose compatibility degradation is stated in the code: an older host misbinds the token and fails validation there, and a cursor secondmate reaching fm-spawn without a grant is refused by the bar. Both paths fail closed. An attended grant cannot describe a worker on another host and is refused for a remote launch. Bootstrap now diagnoses a cursor crew-dispatch rule instead of validating clean and failing later at spawn, since those rules resolve the harness for crewmate and scout spawns and both are barred. Also adds a contains mode to the bootstrap row runner with a guard that fails on an unknown mode; the previous case statement let an unrecognized mode pass vacuously.
…lign guards and docs
…cept legacy carrier
…and recovery docs
…, validate wire grant
… share grant owner
…n-inherited refusal
…erences Each harness reference now owns its task-kind boundary, matching the pattern Kimi's primary limit already used. Muse's restriction moves from the router's Non-negotiable safety section into a Task kinds row in the Muse reference, with the rationale that was split across the router and the maturity section, and AGENTS.md section 4 compresses the cursor unattended-bar block to the trigger, the two grants, and the ownership pointer. The grant mechanics the compressed block carried - never-ambient grants and the local-and-remote refusal scope - land in the Cursor reference, which already owned the rest. Dispatch's owner list now names fm_control_harness_supports_kind as the one owner of task-kind compatibility instead of attributing it to fm-spawn.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported remote exemption metadata divergence is corrected by persisting the exemption reported by the live endpoint. Reviews (2): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d558b8e3ad
ℹ️ 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".
| printf '%s' 'codex __MODELFLAG____EFFORTFLAG__-s workspace-write -a never -c sandbox_workspace_write.network_access=true "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' | ||
| else | ||
| printf '%s' 'codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' | ||
| printf '%s' 'codex __MODELFLAG____EFFORTFLAG__-s workspace-write -a never -c sandbox_workspace_write.network_access=true -c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' |
There was a problem hiding this comment.
Restore writable supervisor paths for Codex workers
Captain, for every Codex ship or scout, this template selects workspace-write, while the preceding comment acknowledges that this denies supervision paths outside the task worktree. Generated briefs require writes to $FM_HOME/state/<id>.status, inbox acknowledgement directories, and—for scouts—$FM_HOME/data/<id>/report.md, all outside that worktree. Consequently Codex workers cannot acknowledge steering or publish blocker, completion, or report artifacts, so they can appear permanently silent even when their work ran; grant those paths explicitly or move the durable channels into a writable root.
AGENTS.md reference: AGENTS.md:L519-L520
Useful? React with 👍 / 👎.
| echo "kind=secondmate" | ||
| echo "mode=secondmate" | ||
| echo "yolo=off" | ||
| [ -z "$CURSOR_EXEMPTION" ] || echo "cursor_exemption=$CURSOR_EXEMPTION" |
There was a problem hiding this comment.
Record the actual grant on reused remote endpoints
When an already-alive remote Cursor secondmate is retried with a new envelope grant, the remote cmd_launch returns its existing route without applying the requested grant, and print_route does not report the endpoint's recorded exemption; this line nevertheless publishes the caller's new value into the parent metadata. The parent can therefore claim that an old endpoint—particularly one created before this change—runs under an exemption it never received, and later audits or recovery inherit that false safety posture. Return and record the remote endpoint's actual cursor_exemption, or refuse reuse when it differs from the requested grant.
AGENTS.md reference: AGENTS.md:L200-L201
Useful? React with 👍 / 👎.
| # spanning several modes is two invocations rather than a silent mixed dispatch. | ||
| [ "$MODE_SET" -eq 0 ] || shared_args+=(--mode "$MODE") | ||
| [ "$YOLO_SET" -eq 0 ] || shared_args+=(--yolo "$YOLO") | ||
| [ "$CURSOR_EXEMPTION_SET" -eq 0 ] || shared_args+=(--cursor-exemption "$CURSOR_EXEMPTION") |
There was a problem hiding this comment.
Reject attended exemptions in batch dispatch
When a batch contains multiple tasks, this line forwards one --cursor-exemption attended token to every child invocation. The token asserts that a person is present in that specific worker pane, but each child creates a separate concurrently live pane and batch dispatch never places the operator in those panes. A single assertion therefore lets every Cursor worker past the unattended bar, leaving classifier prompts in unwatched panes parked indefinitely; reject attended for batch mode or require individual invocations, while continuing to allow a shared durable envelope grant.
AGENTS.md reference: AGENTS.md:L200-L201
Useful? React with 👍 / 👎.
| if fm_control_harness_family "$harness" >/dev/null 2>&1 && | ||
| ! fm_control_harness_supports_kind "$harness" "$kind" "$grant"; then |
There was a problem hiding this comment.
Detect Cursor behind raw-command wrappers
For a supported raw command such as env -u CLAUDECODE cursor-agent ..., the raw-command parser records the first non-assignment word (env) as the harness. This new conditional then fails to map that wrapper to a known family and skips the kind check entirely, even though the pane subsequently executes Cursor; command, exec, and nice wrappers have the same result. Such launches bypass the unattended grant requirement and also omit Cursor-specific tracking, so resolve past standard wrappers or refuse wrapped commands that execute a verified restricted adapter.
AGENTS.md reference: AGENTS.md:L200-L201
Useful? React with 👍 / 👎.
Intent
Consolidate Cursor and Muse worker-kind restriction ownership in firstmate's shared tracked material - the bounded follow-up deliberately excluded from PR 3384. The harness-adapters router skill stated the Muse kind restriction inline while the broader Cursor unattended-worker rule lived in a harness reference and AGENTS.md; pick one authoritative placement for comparable restriction contracts using the one-owner rule and replace duplicate statements with concise trigger/pointer text. Preserve every existing safety boundary, adapter verification fact, runtime distinction, and trigger. Update deterministic tests and documentation-audience ownership where needed, and do not broaden into unrelated harness restructuring. Accepted placement: each harness's reference page owns its task-kind boundary (as Kimi's primary limit already did), with enforcement owned by fm_control_harness_supports_kind in bin/fm-control-lib.sh; the compressed AGENTS.md section 4 keeps the bar, the two cursor-exemption grants, the route-away directive, and ownership pointers, with its unique grant facts absorbed into references/harness/cursor.md. The branch is stacked on PR 3384's head 995a555 (origin/fm/harden-crew-launch-sandbox) because the contracts it consolidates were introduced there.
What Changed
bin/fm-spawn.shlaunches Claude with--permission-mode autoplusCLAUDE_CODE_DISABLE_FAST_MODE=1, Codex with-s workspace-write -a neverand an explicitsandbox_workspace_write.network_access=truegrant, and Cursor with--auto-review --sandbox enabledin place of--yolo/--force.--cursor-exemption attendedor--cursor-exemption envelope:<name>. The grant is validated per invocation, recorded ascursor_exemption=in the task meta, refused outright on a non-cursor harness, and inherited across a relaunch or--secondmaterespawn only in theenvelope:form;bin/fm-control.sh relaunchaccepts the flag and evaluates the effective grant before it stops the running agent.bin/fm-remote-secondmate-control.shcarries the envelope grant over the wire and refusesattended, andbin/fm-bootstrap.shreports a schema-valid crew-dispatch file namingcursoras a separateCREW_DISPATCH:advisory rather than as invalid.fm_control_harness_supports_kindand its helpers inbin/fm-control-lib.sh, so one predicate answers for both the launch owner and the control plane. Each harness reference under.agents/skills/harness-adapters/references/harness/now owns its own task-kind boundary (Muse's crewmate/scout limit moved out of the router skill, cursor's grant mechanics intocursor.md), the compressedAGENTS.mdsection keeps the bar and points at the owners, and the test suites for spawn dispatch, control relaunch, bootstrap, secondmate harness, and the remote trace-context route were extended to cover the new refusals and inheritance rules.Risk Assessment
✅ Low: Documentation-only consolidation with no executable change; every ownership claim, pointer target, and preserved fact was verified against bin/fm-control-lib.sh, bin/fm-spawn.sh, bin/fm-control.sh, and the named docs, and no fact or safety boundary was lost.
Testing
I ran the six targeted suites that own the contracts this consolidation points at — fm-control, fm-control-relaunch, fm-harness-adapter-references, fm-documentation-audiences, fm-cursor-harness and fm-secondmate-harness — and all passed, then went past unit-level confirmation and drove the real operator CLIs to capture two transcripts: eleven fm-spawn.sh and fm-control.sh invocations showing muse refused as a secondmate but allowed as a scout, cursor refused for unattended ship/scout/secondmate, both exemption grants accepted and recorded in task meta while an ambient exported variable and an unnamed envelope are not, a grant on a non-cursor harness refused rather than recorded, and both refusals landing while the live pane process is still the original agent, which is the pre-stop guarantee AGENTS.md now delegates to docs/agent-control.md. There is no UI surface here — the change is Markdown that firstmate reads plus the CLI refusals it governs — so the reviewer-visible artifacts are CLI transcripts rather than screenshots. One suite, fm-muse-harness, cannot complete on this machine because macOS kills a copied /bin/bash; that is a host limitation unrelated to a documentation-only change, and the muse boundary it would have covered is demonstrated in the transcript instead.
Evidence: Worker-kind boundary — end-user CLI transcript (bin/fm-spawn.sh)
Source: Worker-kind boundary — end-user CLI transcript (bin/fm-spawn.sh)
=== [2] muse secondmate - forbidden kind === $ bin/fm-spawn.sh muse-secondmate-e2 <home> --harness muse --secondmate error: muse is a verified crewmate/scout adapter only and cannot run a secondmate; it has no primary supervision protocol. Select a harness verified for secondmates. [exit status: 1] [harness process launched: no] === [4] cursor scout with CURSOR_EXEMPTION/FM_CURSOR_EXEMPTION exported - grant is never ambient === $ bin/fm-spawn.sh cursor-scout-e4 <project> --scout --harness cursor --model cursor-grok-4.5-high error: cursor is a verified adapter but is refused for an unattended scout launch: ... pass it on the invocation itself with --cursor-exemption attended or --cursor-exemption envelope:<name> ... [exit status: 1] [harness process launched: no] === [5] cursor scout with --cursor-exemption attended - granted === spawned cursor-scout-e5 harness=cursor kind=scout cursor_exemption=attended window=firstmate:fm-cursor-scout-e5 ... [exit status: 0] [recorded task meta] harness=cursor kind=scout cursor_exemption=attended === [8] explicit grant on a non-cursor harness - refused, not recorded === error: --cursor-exemption applies only to a cursor launch, but this spawn resolved harness=claude; drop the flag rather than recording a cursor grant that would outlive it [exit status: 1]Evidence: Control-plane pre-stop kind/grant check — end-user CLI transcript (bin/fm-control.sh relaunch)
Source: Control-plane pre-stop kind/grant check — end-user CLI transcript (bin/fm-control.sh relaunch)
=== [1] relaunch a claude secondmate onto muse (crewmate/scout-only adapter) === [before] live pane process: claude | recorded harness: claude $ bin/fm-control.sh sm-e1 relaunch --harness muse error: muse is a verified crewmate/scout adapter only and cannot run a secondmate; it has no primary supervision protocol. ... Relaunching sm-e1 onto it would stop the running agent for a launch that must be refused. [exit status: 1] [after] live pane process: claude | recorded harness: claude === [2] relaunch a ship task onto cursor with no grant === [before] live pane process: claude | recorded harness: claude error: cursor is a verified adapter but is refused for an unattended ship launch ... Relaunching t-e2 onto it would stop the running agent for a launch that must be refused. [exit status: 1] [after] live pane process: claude | recorded harness: claude === [3] relaunch the same ship task onto cursor WITH --cursor-exemption attended === relaunched t-e3 harness=cursor from=claude model=cursor-grok-4.5-high effort=default backend=tmux endpoint=fmses:fm-t-e3 ... [exit status: 0] [after] live pane process: cursor-agent | recorded harness: cursorPipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed (2) ✅
AGENTS.md:200- The compressed line says cursor "stays available only through the per-spawn--cursor-exemption attendedor--cursor-exemption envelope:<name>grant". The word "only" is stronger than the implemented behavior: anenvelope:<name>grant is inherited from the task's own record on the two restart paths, so firstmate's own secondmate liveness recovery re-runsfm-spawn.sh <id> --secondmatewith no flag and succeeds (bin/fm-spawn.sh:1500 and the remote branch at bin/fm-spawn.sh:527, exercised by tests/fm-remote-secondmate-trace-context.test.sh's "recovery: an unflagged respawn inherits the recorded envelope grant"). The removed block stated the inheritance rule explicitly. An agent reading only AGENTS.md would conclude an unflagged enveloped respawn must be refused. Mitigated by the next line pointing at the harness reference for inheritance, and the intent explicitly scopes this block to "the bar, the two cursor-exemption grants, the route-away directive, and ownership pointers" - so this is flagged rather than changed.AGENTS.md:201- The pointer claims cursor's "grant mechanics, inheritance, and audit rules" are owned by "its harness reference inharness-adapters". Two facts carried by the block this line replaced are not in references/harness/cursor.md: that a LOCAL cursor secondmate is never auto-recovered and is reported asunverified-harness(only docs/configuration.md:313 - cursor.md:73 states the remote-only half but not the local reporting/remedy), and that the pre-stop check is keyed on the effective grant rather than the rawcursor_exemption=record (only docs/agent-control.md:98). No fact was lost from the repository, but a reader following this pointer will not find them where it says they live.🔧 Fix: scope cursor grant to fresh launches, name distributed owners
3 issues (1 warning, 2 infos) still open:
.agents/skills/harness-adapters/references/harness/cursor.md:68- The line absorbed from AGENTS.md drops a scoping word and becomes self-contradicting: "neither an exported variable nor an earlier launch can exempt a later unattended spawn." Line 72 of the same file states the opposite for envelope grants - "Across a relaunch or a--secondmaterespawn ... anenvelope:<name>grant is inherited" - and a--secondmaterespawn IS a later unattended spawn exempted by an earlier launch's record (bin/fm-spawn.sh:1506-1509 and the remote branch at bin/fm-spawn.sh:528-530). The AGENTS.md text this replaced was precise: "one ATTENDED launch cannot silently exempt a later unattended spawn", and docs/configuration.md:305 scopes it the same way ("cannot leak to a later spawn IN THE SAME SHELL"). As written, the reference that is now the one owner of the grant contract contradicts its own inheritance rule four lines down. Fix: restore the scoping, e.g. "neither an exported variable nor an earlier ATTENDED launch can exempt a later unattended spawn". The exported-variable half is accurate - bin/fm-spawn.sh:323 unconditionally clears CURSOR_EXEMPTION before parsing.AGENTS.md:201- The sentence widens whatfm_control_harness_supports_kindenforces beyond what it does: "Each harness's complete task-kind boundary, including cursor's grant mechanics, restart inheritance, and audit rules, is owned by its harness reference ..., withfm_control_harness_supports_kind... enforcing IT on every spawn and before a relaunch stops the running agent." The referent of "it" is that whole list, but bin/fm-control-lib.sh:127-136 only answers kind-plus-grant admissibility. Restart inheritance is owned by a different function,fm_control_cursor_exemption_inherited(bin/fm-control-lib.sh:194), applied by bin/fm-spawn.sh:1506; the audit record is written by bin/fm-spawn.sh:747/3045. The refusal of an explicitly passed grant on a non-cursor harness is also a sibling rule insidefm_control_launch_refusal(bin/fm-control-lib.sh:298-301), not inside supports_kind. The pre-existing pointers got this right by keeping the object narrow - docs/configuration.md:315 and cursor.md:20 both say it owns "that rule" (the kind rule) only. Since the whole point of this change is accurate one-owner attribution, consider narrowing "it" to the kind boundary, or namingfm_control_launch_refusalas the composer both spawn routes and the relaunch actually call.AGENTS.md:202- The fix round's new third sentence does not just name the owners, it restates both facts in full: "a local cursor secondmate never being auto-recovered, reported instead asunverified-harnessand brought back only by an explicit relaunch" is docs/configuration.md:313 restated, and "the pre-stop relaunch check being keyed on the grant that will really be in force rather than on the recordedcursor_exemption=value" is docs/agent-control.md:98 restated. That re-creates in AGENTS.md the duplication this change exists to remove, and it partly reinstates two of the eight lines the consolidation deleted. The prompting instruction asked only that the two facts be "findable where they live", which a bare pointer satisfies - e.g. "the harness-support section ofdocs/configuration.mdowns local cursor secondmate recovery, anddocs/agent-control.mdowns what the pre-stop relaunch check is keyed on." The pointers themselves are accurate: I confirmed line 313 sits under the## Harness supportheading (line 293) and that agent-control.md:98 states exactly the effective-grant keying.🔧 Fix: scope never-ambient rule to attended, narrow ownership pointers
✅ Re-checked - no issues remain.
tests/fm-muse-harness.test.sh:176- tests/fm-muse-harness.test.sh cannot run to completion on this machine: its first case (test_detects_versioned_process_ancestor) copies /bin/bash tomuse-bin-0.1.0-R708.1and executes it, but macOS SIGKILLs the copy because /bin/bash is an Apple platform binary whose code signature is invalid at a new path (reproduced standalone: exit 137). No non-Apple bash is installed to substitute. This is a host limitation, not a product failure, and is unrelated to this documentation-only change (bin/ and tests/ are untouched by it). The muse secondmate refusal that this change's doc consolidation depends on was instead exercised directly through the fm-spawn.sh CLI and via tests/fm-control.test.sh; remote CI owns the detection coverage.bin/fm-test-run.sh tests/fm-control.test.sh— the one-owner predicate: per-kind capability, muse secondmate refusal, cursor unattended bar and grant token validation (passed)bin/fm-test-run.sh tests/fm-control-relaunch.test.sh— 58 assertions covering the pre-stop refusal invariant, attended-vs-envelope grant inheritance, and grant refusal on a non-cursor target (passed)bin/fm-test-run.sh tests/fm-harness-adapter-references.test.sh— the harness-adapter-routing-v1 artifact is still a normalized map and every reference target resolves after the SKILL.md edit (passed)bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh— documentation-audience classification and "required documentation owner pointers cannot silently disappear" (passed)bin/fm-test-run.sh tests/fm-cursor-harness.test.sh(passed)bin/fm-test-run.sh tests/fm-secondmate-harness.test.sh— includes the attended/unattended cursor secondmate launch contract (passed)bin/fm-test-run.sh tests/fm-muse-harness.test.sh— FAILS on this host only, at the copied-/bin/bash process-ancestry detection case (see finding)Manual CLI evidence run: 8 realbin/fm-spawn.shinvocations (muse scout, muse --secondmate, cursor scout bare / with ambient env vars / --cursor-exemption attended / envelope:routing-benchmark / envelope: / grant on --harness claude) against a fake tmux pane, recording exit status, whether a harness process was launched, and the resulting task metaManual CLI evidence run: 3 realbin/fm-control.sh <id> relaunchinvocations (secondmate onto muse, ship onto cursor bare, ship onto cursor with --cursor-exemption attended) against a lifecycle-modelling tmux stub, recording the live pane process and recorded harness before and after each callgrep -nverification that the AGENTS.md ownership pointers resolve to real sections in docs/configuration.md and docs/agent-control.md, and that references/harness/kimi.md still states its own primary limitdocs/configuration.md:304- Ownership contradiction left by the consolidation, worth a follow-up rather than a broader restructuring here. AGENTS.md:201 now declares that each harness reference owns "cursor's grant mechanics, restart inheritance, and audit rules", but docs/configuration.md's "Harness support" section (lines 304-311) still carries a full parallel account of exactly those facts (per-invocation grant, cursor_exemption= recording, envelope charset, non-cursor refusal vs. inherited drop, envelope-vs-attended inheritance), and docs/verification/trace-context.md:22 points back at that section as the owner of "the rule". Nothing is currently wrong, but three surfaces now restate one contract, so the next behavior change has to be applied in all three or one goes stale. Follow-up: decide whether configuration.md's operator-current copy is reduced to the bar plus a pointer to references/harness/cursor.md (keeping only the local-vs-remote recovery asymmetry AGENTS.md assigns to it), and retarget the trace-context.md owner pointer accordingly. Left alone here because the intent explicitly scopes this change to AGENTS.md, the router skill, and the harness references, and forbids broadening into unrelated harness restructuring.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.