Skip to content

fix: keep worker launch briefs off command lines - #111

Merged
Freudator86 merged 4 commits into
mainfrom
fm/fm-brief-off-argv
Aug 16, 2026
Merged

fix: keep worker launch briefs off command lines#111
Freudator86 merged 4 commits into
mainfrom
fm/fm-brief-off-argv

Conversation

@Freudator86

@Freudator86 Freudator86 commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Harness verification: what was measured, and what was not

The acceptance bar for this change is per harness, not per fleet, so it is stated per harness here.
A pass requires both halves: the sentinel worker received and followed its brief, and a host-wide scan of process command lines found its unique marker nowhere. A worker that never got its brief is not a pass.

Harness Version here Verified by sentinel spawn Evidence
claude 2.1.233 YES before: brief followed, marker readable on pid 3832468 (827-byte command line). after: brief followed, marker in 0 of 474 processes
codex codex-cli 0.145.0 YES before: brief followed, marker readable on pid 3881761 (960-byte command line). after: brief followed, marker in 0 of 480 processes
opencode not installed UNVERIFIED no opencode binary on this host, so no worker could be launched to measure
pi not installed UNVERIFIED no pi binary on this host; the Pi calm-mode E2E also skips here for the same reason
grok not installed UNVERIFIED no grok binary on this host

The three UNVERIFIED harnesses have their launch templates converted, and tests/fm-spawn-brief-off-argv.test.sh executes their launch composition and reads the resulting argv. That is real execution rather than code inspection, but it is not the same evidence as a sentinel spawn and is deliberately not reported as such. There is no blanket "all five clean" claim in this PR: three of the five rest on composition evidence alone, and docs/brief-off-argv.md records that boundary with the same wording.

Every sentinel ran in an isolated firstmate home on a private tmux server against a real pooled worktree, so nothing touched the live fleet, and no brief body, credential, or environment was printed at any point.


Intent

Deliver a worker's launch brief without ever placing it on a command line, for every harness this fleet can launch.

The defect, established by a model panel rather than assumed: every launch template in bin/fm-spawn.sh composed the whole encoded brief into the command sent to the pane, so the host process table carried, continuously, the full instructions every agent on the host was working from - task, repository, acceptance criteria, internal paths, operating rules. This was measured on this host on 2026-08-16, not inferred: an ordinary process listing from one account returned another account's entire worker brief (uid 1000, 9820-byte command line, read from uid 1002), plus a sibling worker's brief under the reader's own account.

There are TWO leaks and this change is the only one that closes both. Cross-account: any account on this host reads any other's command lines; persistent process-listing protection would close that half, needs root, and is tracked elsewhere. Same-account: two workers under ONE account read each other's briefs, and process-listing protection does not protect same-uid processes from the same uid, so only taking the brief off the command line closes that half. Containerising the vessels was considered by a panel and explicitly REJECTED for this purpose, because a container relocates the exposure rather than closing it - the launch lands on the host's process table one layer up. Do not propose containers as a substitute.

Constraints the user set: no harness may lose any launch capability it has today; the supervised interactive launch path is the one that matters and a working non-interactive prompt-mode variant does not demonstrate the fix; the mechanism must be chosen per harness against what that harness's INSTALLED CLI actually supports on this machine, verified against the installed version rather than documentation or memory; and secrets discipline applies throughout, so no brief body, credential, or full environment is ever printed while testing.

Decisions made while doing the work. Sanctioned candidates were a file pointer the harness reads, standard input, or terminal input sent after launch. claude 2.1.233 and codex-cli 0.145.0 were both checked with --help: neither has a prompt-file flag on its supervised interactive path, and piping stdin to claude switches it to non-interactive print mode, which leaves the supervised path entirely. So the file pointer - the first sanctioned candidate - is the one mechanism all five harnesses share: the launch carries the brief's PATH and the worker opens the file itself, so the brief's bytes travel filesystem-to-worker and only its address travels argv-to-worker. This is deliberately not weaker than terminal input, because data//brief.md is the durable record and exists on disk under every candidate, so the process-table channel is the only thing any of them could close.

New fm-operational-input.sh launch-pointer <brief-path> builds that launch input; it deliberately never reads the brief, keeps the existing launch-brief wire kind so every consumer that classifies a launch input is unaffected, and fails closed on an unreadable path rather than launching a worker at a brief that is not there. All eight templates in launch_template() use it, crewmate and secondmate shapes alike.

A trap the panel had already found: at least one adapter ALREADY set a brief-path variable while still passing the full encoded brief on the same line. That was pi's FM_FIRSTMATE_PI_LAUNCH_BRIEF, which feeds calm-mode visibility and was never a delivery mechanism. .pi/extensions/fm-calm.ts now derives the value it expects through the same shell owner instead of reading the brief and encoding its body, so the two producers cannot drift; a half-converted path reads as done and is not, so the test asserts the variable and the body independently.

The raw-launch escape hatch for unverified adapters is caller-authored, so it warns rather than refusing - refusing would remove a launch shape that works today, which the "no harness loses any capability" constraint forbids.

Acceptance criteria the user set, deliberately harness by harness. For each harness: a real sentinel worker spawned through the ordinary supervised path carrying a unique marker; it receives and follows its brief; and a host-wide scan of process command lines finds that marker nowhere - both halves required, since a worker that never got its brief is not a pass. Code inspection alone satisfies no criterion. A harness that cannot be exercised on this host must be named UNVERIFIED individually with the reason, and never folded into a general pass - a blanket "all five clean" resting on three measurements is the exact failure this task exists to prevent. A colocated test must cover the launch-path composition so a later edit cannot silently put a brief back on the command line, and must be proven to fail without the change. The PR must state plainly which harnesses were verified by sentinel spawn, which were not, and why.

What was measured. Before the change, sentinel spawns on claude and codex each received and followed their brief AND had their marker readable in the process table (claude pid 3832468, 827 bytes; codex pid 3881761, 960 bytes). After the change, both again received and followed their brief, and a host-wide scan found the marker in 0 of 474 and 0 of 480 processes respectively. Every sentinel ran in an isolated firstmate home on a private tmux server so nothing touched the live fleet. opencode, pi, and grok are UNVERIFIED individually because none of those binaries is installed on this host, so no worker could be launched to measure; their templates are converted and exercised by the executing composition test, which is deliberately not reported as equivalent evidence. docs/brief-off-argv.md records all of this with versions and exact output.

The regression gate is tests/fm-spawn-brief-off-argv.test.sh. It does not inspect source: for every harness, and for the separate secondmate template shapes, it spawns through fm-spawn and then EXECUTES the captured launch command with the harness replaced by an argv recorder, failing if a sentinel that exists only in the brief body appears on any argv. Executing it is the point, because a $(cat brief) and a $(... launch-pointer path) are equally innocent as strings and only the shell can say which one puts a brief on a command line. It also asserts the launch still DELIVERS - the argv names the brief and carries the launch-brief kind - so a launch that leaks nothing because it delivers nothing cannot pass, and a coverage gate fails if an adapter is added to launch_template() with no case. With the templates reverted to their pre-change shape, every case fails on the argv measurement; that was verified per test function.

This repository is firstmate's own shared tracked material and the pin source, so bin/ changes land here and reach the fleet by pin bump. Knowledge placement follows the repo's firstmate-coding-guidelines: mechanics in the script header and --help, empirical evidence in docs/, one line of reinforcement at the adapter-verification risk point in the harness-adapters skill, and deliberately nothing added to AGENTS.md, whose token cost every session pays.

Note for the reviewer: tests/fm-arm-pretool-check.test.sh fails in this worktree, and that failure was confirmed identical on a pristine tree with none of these changes applied. It is pre-existing and unrelated to this work.

What Changed

  • Replace full brief bodies in all supervised harness launch templates with launch-brief file pointers, while preserving crewmate and secondmate launch capabilities and warning on unsafe caller-authored raw commands.
  • Add a fail-closed launch-pointer operational input and align Pi calm-mode visibility with the same pointer construction.
  • Add executed argv regression coverage and document sentinel verification: Claude and Codex verified clean; OpenCode, Pi, and Grok remain unverified because their binaries are not installed on this host.

Risk Assessment

✅ Low: The change is well-bounded, preserves all eight supervised launch shapes, keeps brief bodies off argv through the shared launch-pointer boundary, and the follow-up replaces the flagged source assertions with behavioral coverage without weakening executed harness cases.

Testing

Focused executable checks validated pointer construction, fail-closed operational-input behavior, supervised launch composition for every supported crewmate and secondmate template, Pi’s independent path binding, and dispatch-profile compatibility; all applicable checks passed, while Pi’s installed-package and interactive E2E checks skipped because Pi is unavailable on this host. The sanitized transcript provides reviewer-visible CLI evidence without exposing any brief body, credential, or environment.

Evidence: Sanitized targeted test transcript
ok - operational input: every current generic envelope retains its exact structured kind
ok - operational input: a launch pointer carries the brief path and never the brief
ok - operational input: the established from-firstmate carrier remains structurally typed and byte-compatible
ok - operational input: untyped landed FIRSTMATE_OP transcripts are explicit legacy-operational input
ok - operational input: historical prose compatibility is isolated from current parsing
ok - operational input: quoted, ASCII-only, arbitrary-U+2063, altered-legacy, and label-only near misses stay genuine
ok - operational input: the OpenCode adapter constructs through the canonical owner
ok - operational input: current construction rejects legacy kinds and empty bodies
ok - a claude crewmate launch carries the brief's path, never its body
ok - a codex crewmate launch carries the brief's path, never its body
ok - an opencode crewmate launch carries the brief's path, never its body
ok - a pi crewmate launch carries the brief's path, never its body
ok - a grok crewmate launch carries the brief's path, never its body
ok - secondmate launches carry the charter's path, never its body
ok - pi's brief-path binding coexists with a body that never reaches argv
ok - every harness launch_template() accepts is exercised by a brief-off-argv case

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed ✅
  • 🚨 tests/fm-calm-pi-extension.test.sh:83 - This newly added static contract block only reads TypeScript source and checks for implementation strings. Remove it or replace it with behavior through the Pi extension interface; the later pointer-classification fixture already exercises the relevant observable behavior.
  • 🚨 tests/fm-spawn-brief-off-argv.test.sh:263 - These tests parse launch_template() source and search for case labels and forbidden strings, which violates the test-quality rule. Retain the executed argv cases, and expose/consume a semantic supported-harness registry if exhaustive coverage must be enforced.
  • 🚨 tests/fm-captain-translation-contract.test.sh:255 - The changed assertion reads fm-spawn.sh and checks for a literal constructor string rather than exercising emitted operational input. Remove it or assert the kind through a captured and executed launch command.

🔧 Fix: Replace source assertions with behavioral harness registry
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-operational-input.test.sh
  • bash tests/fm-spawn-brief-off-argv.test.sh
  • bash tests/fm-calm-pi-extension.test.sh
  • bash tests/fm-spawn-dispatch-profile.test.sh
  • Installed-version check via command -v and &lt;harness&gt; --version: Claude 2.1.233 and codex-cli 0.145.0 available; opencode, pi, and grok unavailable
  • Repeated tests/fm-operational-input.test.sh and tests/fm-spawn-brief-off-argv.test.sh while capturing the sanitized evidence transcript
  • git status --short confirmed testing left no transient worktree changes
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Clarify empty local declaration
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

Bridge CI added 4 commits August 16, 2026 18:38
Every launch template composed the whole encoded brief into the command sent
to the pane, so the host process table carried, continuously, the full
instructions every agent on the host was working from: task, repository,
acceptance criteria, internal paths, operating rules.

Measured on this host, not inferred. A sanitised scan of /proc/*/cmdline from
one account returned another account's entire worker brief (uid 1000, 9820-byte
command line, read from uid 1002) and a sibling worker's brief under the reader's
own account (11511 bytes). Both home directories are mode 750 owned by their own
account, so the process table was the only channel through which a brief crossed
the account boundary, and process-listing protection would not have closed the
same-uid half at all.

Each installed CLI was checked for a native prompt-file flag on its supervised
interactive path; neither claude 2.1.233 nor codex-cli 0.145.0 has one, and
piping stdin to claude leaves the supervised path entirely. So the launch now
carries the brief's PATH and the worker opens the file itself: the brief's bytes
travel filesystem to worker, and only its address travels argv to worker.

bin/fm-operational-input.sh gains `launch-pointer <brief-path>`, which builds the
launch input from the path and never reads the brief, keeps the launch-brief wire
kind so every consumer that classifies a launch input is unaffected, and refuses
an unreadable path rather than launching a worker at a brief that is not there.
All eight templates in bin/fm-spawn.sh's launch_template() use it, crewmate and
secondmate shapes alike.

Pi already set FM_FIRSTMATE_PI_LAUNCH_BRIEF to the brief's path while the full
encoded brief still rode the same line. That variable feeds calm-mode visibility
and was never a delivery mechanism, so .pi/extensions/fm-calm.ts now derives the
value it expects through the same shell owner instead of reading the brief and
encoding its body.

tests/fm-spawn-brief-off-argv.test.sh is the regression gate. For every harness,
and for the separate secondmate shapes, it spawns through fm-spawn, then EXECUTES
the captured launch command with the harness replaced by an argv recorder and
fails if a sentinel that exists only in the brief body appears there. Executing it
is the point: a $(cat brief) and a $(... launch-pointer path) are equally innocent
as strings. It also asserts the launch still delivers, so a launch that leaks
nothing because it delivers nothing cannot pass, and a coverage gate fails if an
adapter is added with no case. With the templates reverted, every case fails on
the argv measurement.

Verified by sentinel spawn on this host: claude and codex, each receiving and
following its brief with the marker then found in no host command line.
UNVERIFIED individually: opencode, pi, and grok, none of which is installed here.
docs/brief-off-argv.md records the measurements, versions, and that boundary.
@Freudator86
Freudator86 merged commit 8725f81 into main Aug 16, 2026
11 checks passed
@Freudator86
Freudator86 deleted the fm/fm-brief-off-argv branch August 16, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant