Skip to content

fix(bin): pre-register Codex workspace trust before spawn - #6731

Open
andrewkimcanidium wants to merge 8 commits into
kunchenguid:mainfrom
andrewkimcanidium:fm/fm-codex-trust-preregister
Open

andrewkimcanidium wants to merge 8 commits into
kunchenguid:mainfrom
andrewkimcanidium:fm/fm-codex-trust-preregister

Conversation

@andrewkimcanidium

@andrewkimcanidium andrewkimcanidium commented Oct 6, 2026 •

Copy link
Copy Markdown

Intent

Okay, what can we dispatch? (Captain chose 2026-10-06: CPQ research pack, Drive how-to inventory, fleet fixes, usage-axi source registry.)

Backlog note:
"Firstmate pre-registers workspace trust for Claude workers before launch, and does not do the equivalent for Codex. The asymmetry is visible in two ways.

A Claude spawn that cannot pre-register refuses outright rather than launching: "could not pre-register Claude workspace trust for ; refusing to launch a claude worker that would wedge on the trust dialog". That is the guard working, and it is good behaviour.

A Codex spawn has no such step, so every worker launched into a fresh treehouse worktree stops on Codex's own "Trust this folder?" prompt and waits for a human. Observed repeatedly on 2026-10-02 across sap-cpq-ci, score-varicent, sap-im-doc-survey and others. Each one needs the captain to press enter before any work starts, which is precisely the interruption the Claude path was built to prevent.

The captain's own workaround, applied locally on 2026-10-02: add a trust entry for the treehouse root to ~/.codex/config.toml, which covers every current and future worktree in one line:

[projects."~/.treehouse"]
trust_level = "trusted"

Per-project entries are not enough, because workers never run in the project clone - they run in a disposable worktree under a different root, and Codex resolves trust from the path it is launched in.

Worth contributing upstream rather than keeping local, since any firstmate home dispatching Codex workers into treehouse worktrees hits this on every new worktree. The fix likely belongs beside the existing Claude pre-registration in the spawn path, so both verified harnesses behave the same way; a configuration note in the documentation would be a weaker second choice because it leaves the asymmetry in the code."

What Changed

  • Adds bin/fm-codex-trust.sh. It trusts only the exact launch directory: either an isolated linked worktree of the given project or a seeded secondmate home. Never an ancestor, the treehouse root or a primary checkout. It writes the entry into ${CODEX_HOME:-~/.codex}/config.toml through Codex's private stdio app-server config API (config/read + config/value/write), not by editing the TOML directly. It refuses in these cases:

    • CODEX_HOME is relative
    • the config is malformed
    • the directory already has an explicit untrusted entry
    • the store is a symlink or owned by another user

    A version conflict or a lost concurrent write is re-read and retried, up to 3 attempts. The script reads the entry back to confirm it was saved before reporting success. Its app-server is always shut down, with SIGKILL if it hasn't stopped 2 seconds after SIGTERM.

  • bin/fm-spawn.sh now runs this helper before every codex worker or secondmate launch. It refuses the spawn if the helper fails, the same way the Claude path does. It also passes the resolved CODEX_HOME into the launch command, so registration and launch read the same store.

  • Test and doc changes:

    • Fixture: tests/lib.sh gives each suite a private CODEX_HOME and puts a config-only codex wrapper (tests/codex-config-fixture.js) first on PATH, so non-live spawns never need an installed codex or touch the operator's real store. fm_live_gate undoes both for live guards.
    • New suites: tests/fm-codex-trust.test.sh (portable) and the opt-in tests/fm-codex-workspace-trust-live-e2e.test.sh, both registered in bin/fm-test-run.sh.
    • Existing spawn suites: updated to use the fixture.
    • Docs: the harness-adapter references, CONTRIBUTING.md, docs/orca-backend.md and docs/verification/runtime-backends.md now describe Codex pre-registration.

🤖 Generated with Claude Code

Risk Assessment

⚠️ Medium: The change meets the intent: Codex trust is pre-registered beside the Claude step, scoped to each launch directory, and a failed registration refuses the spawn. The stalled-shutdown fix is correct. It does, however, put every codex spawn behind a new external Codex config API and a strict fail-closed store check, which can block codex dispatch entirely on some hosts.

Testing

I re-ran the Codex trust unit suite and the live end-to-end check against the real codex-cli 0.159.3: a registered worktree reached the composer without the trust prompt. I then re-drove the round-5 concurrency failure live against the real app-server: 138 registrations raced into one shared disposable store, and none were refused or lost. The secondmate-harness suite from the latest CI fix passes, and the operator's real Codex store was left byte-identical. Two scenarios were checked only by the unit suite, with fixtures, so they are untested live: the fail-closed refusals and spawn refusal, and the kill of a server that stalls on shutdown. All temporary directories were removed.

  • Live validation: ✅ go - 3 of 5 scenarios driven live against the product
Scenario Result Live Evidence
A Codex worker launched into a fresh linked worktree reaches its composer with no 'Trust this folder?' prompt, while an unregistered checkout still shows the prompt ✅ pass live r6-live-e2e.log (real codex-cli 0.159.3 TUI)
Adversarial: concurrent Codex spawns sharing one store all register through the bounded re-read and retry, including writes lost to a concurrent replace ✅ pass live r6-race-real-codex.log, r6-race-8way.log: 0 refusals and 0 missing entries across 138 registrations; the pre-existing config key was preserved
Primary checkouts, unrelated paths, explicitly untrusted entries, malformed configs and symlinked stores are refused; a spawn whose registration fails refuses before launching ⏸️ untested no The earlier payload did not establish a live result. This scenario was run only through the helper with the config fixture, and through fm-spawn inside the test harness (tests/fm-codex-trust.test.sh).…
A Codex server that stalls on shutdown is killed and the refusal still returns (Greptile P2 decision) ⏸️ untested no The earlier payload did not establish a live result. A real codex can't be made to ignore SIGTERM on demand, so this was checked only with the suite's stalling fixture server in tests/fm-codex-trust.t…
Secondmate spawns in tests find the config-only codex on a minimal PATH and never write the operator's real ~/.codex store ✅ pass live r6-secondmate-harness.log exit 0; checksum of ~/.codex/config.toml unchanged
Evidence: Real-codex concurrent registration race (2-way and 4-way)

Source: Real-codex concurrent registration race (2-way and 4-way)

n=2 rounds=25 refused=0 missing_entries=0 n=4 rounds=12 refused=0 missing_entries=0 project tables: 99 trusted: 99 model = "gpt-5" (pre-existing key preserved)

n=2 rounds=25 refused=0 missing_entries=0
n=4 rounds=12 refused=0 missing_entries=0
project tables: 99
trusted: 99
model = "gpt-5"

[projects."/private/var/folders/6f/1jf88sjj1yb6bwg4qkrtlxd00000gn/T/fm-race.4ED9Me/warm"]
Evidence: 8-way stress race

Source: 8-way stress race

n=8 rounds=5 refused=0 missing_entries=0 (refusals are fail-closed; any success must persist)
Evidence: Live Codex TUI trust e2e

Source: Live Codex TUI trust e2e

ok - codex codex-cli 0.159.3: unregistered checkout gates; registered worktree reaches composer without input
ok - codex codex-cli 0.159.3: native config API preserves TOML, is idempotent and refuses malformed input
# all fm-codex-workspace-trust-live-e2e checks passed
exit 0
Evidence: fm-codex-trust suite

Source: fm-codex-trust suite

ok - fresh worktree trust is idempotent
ok - existing configuration is preserved
ok - primary checkout, subdirectories and unrelated paths are refused
ok - malformed, declined, symlinked and ambiguous stores fail closed
ok - a version conflict that clears is retried and retains concurrent changes
ok - a version conflict past the bound refuses registration and retains concurrent changes
ok - an acknowledged write lost to a concurrent replace is retried
ok - a write lost past the bound refuses registration
ok - a server stalled on shutdown is killed and the refusal still returns
ok - only the named seeded secondmate home is eligible
ok - spawn registers trust and forwards the same CODEX_HOME
ok - spawn refuses before launching when trust registration fails
# all fm-codex-trust checks passed
exit 0
Evidence: fm-secondmate-harness suite

Source: fm-secondmate-harness suite

ok - A1 fm-harness.sh secondmate resolves the fallback chain; crew mode unchanged
ok - fm-harness detects only Cursor Agent CLI's exact invocation marker
ok - C1 fm-harness.sh secondmate-model/secondmate-effort resolve the optional tokens; bare harness stays empty (backward-compat)
ok - pi-signed identity: authoritative launch selection distinguishes shared wrapper ancestry
ok - harness identity: dash-leading ps command names are basename operands, not options
ok - B1 propagate_inheritable_config: copy, idempotence, convergence, absence-mirror, exclusion, no-op, skip diagnostics
ok - B2 spawn: secondmate runs the secondmate harness; its home inherits declared config
ok - B3 spawn: an absent secondmate-harness falls back to the crew harness (backward-compat)
ok - B4 spawn: no config at all -> own harness and no propagation side effects
ok - B5 spawn: an explicit per-spawn harness arg overrides config/secondmate-harness
ok - B6 spawn: an unverified resolved secondmate harness is refused (guard intact)
skip: cursor executable not resolvable in this environment, so the launch could not be built
ok - B5b spawn: FM_BACKEND wins over inherited config/backend
ok - B5c spawn: explicit --backend wins over FM_BACKEND and inherited config/backend
ok - C2 spawn: a bare harness-only secondmate-harness file launches with no model/effort flag (backward-compat)
ok - C3 spawn: config/secondmate-harness's model token threads --model into the launch and meta
ok - C4 spawn: config/secondmate-harness's model+effort tokens thread into the launch and meta
ok - C5 spawn: an explicit --model overrides config/secondmate-harness's model token; the file's effort token still applies
ok - C6 spawn: an explicit --effort overrides config/secondmate-harness's effort token; the file's model token still applies
ok - C7 spawn: an explicit --harness starts with clean model/effort defaults
ok - C8 spawn: an explicit --harness still honors explicit model/effort flags
ok - C9 spawn: secondmate launch pins supervision to its own harness
ok - C9 spawn: the harness fallback chain still resolves with no tokens; crew/scout launches are unaffected by this feature
ok - B7 bootstrap sweep pushes, re-converges, and mirrors absence; never inherits secondmate-harness
ok - B8 bootstrap sweep propagates config even when the home's tracked files are already current
ok - B9 bootstrap sweep defers new inherited config until the home ignores it
ok - B10 bootstrap sweep materializes and inherits the startup-memory default while fast-forwarding
ok - B12b backend inheritance: present values and primary absence converge exactly
ok - C2b spawn: config/claude-permission-mode=auto reaches a Claude secondmate launch
ok - claude secondmate launches cover the parent-home steering inbox in auto and bypass modes
ok - B12c claude-permission-mode inheritance: present values and primary absence converge exactly
ok - B12c presentation inheritance: the primary default converges on, and only an explicit opt-out propagates off
ok - B11 bootstrap sweep surfaces config propagation failures
ok - B11 bootstrap rereads completed config writes after partial propagation
ok - B12 config-push propagates via shared live discovery, reports items, rereads on change only, and does not fast-forward
ok - B13 config-push reports dirty, non-allowing, and invalid homes without failing warnings-only runs
ok - B14 config-push exits nonzero on real propagation errors
ok - B14 config-push rereads completed config writes after partial propagation
ok - B15 config reread is per-home, exact-byte, ordered, and pointer-only
ok - B16 config reread isolation, ABSENT, generation safety, send failure, and retry
ok - B20 config reread publication failures retain exact generations for retry
ok - B21 config reread instruction-write failures retain exact retry generations
ok - B21 config reread preserves exact bytes when temporary adoption also fails
ok - B21 config reread serializes concurrent propagation and delivery
ok - B22 full config reread retry queues drain before new publication
ok - B23 mixed config reread delivery failures still bound sent history
ok - B26 config reread delivery stops after the oldest failed generation
ok - B17 config reread skips unchanged homes and reads destination post-write bytes
ok - B18 bootstrap config reread path works; spawn flexibility remains defaults-only
ok - B19 bootstrap respawns before inherited-config reread
ok - B25 spawn quarantines stale rereads without blocking relaunch
ok - B24 bootstrap detect-only mode remains filesystem read-only
# all fm-secondmate-harness tests passed
exit 0
- Outcome: 🔧 2 issues found → auto-fixed (2) → no changes applied ✅ across 6 runs (1h5m12s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 warning
  • ℹ️ bin/fm-codex-trust.sh:95 - Every codex spawn and relaunch (bin/fm-spawn.sh:4430-4442) now refuses unless node is present and the installed codex answers codex app-server --listen stdio:// with the config/read (includeLayers) and config/value/write (expectedVersion) API (bin/fm-codex-trust.sh:95-132). Before this change, a codex install without that API launched and stopped on the trust dialog. Now it cannot launch codex workers at all. This fail-closed choice matches the Claude guard that the intent calls good behaviour, and it is recorded as verified only on codex-cli 0.159.3 (docs/verification/runtime-backends.md). No action is needed; this is noted so operators on older Codex builds know about the new version floor.

✅ No issues found.

  • ⚠️ bin/fm-codex-trust.sh:38 - ownedRegular uses lstat and requires a regular file. As a result, a ${CODEX_HOME:-~/.codex}/config.toml that is a symlink causes the helper to refuse (lines 41, 87, 114 and 122). Because bin/fm-spawn.sh:4549-4552 treats that refusal as fatal, every codex worker, scout and secondmate spawn fails on that host. Symlinks are the normal layout when dotfile managers (stow, chezmoi, home-manager) manage config.toml. Before this change such a host launched codex and showed the trust prompt. Now it cannot launch codex at all, and the error claims the worker 'would wedge on the trust dialog'. Failing closed on symlinked stores is a deliberate, documented choice, and probably protects against Codex's atomic replace overwriting the link. The user should still confirm that blocking every codex spawn is preferred over, for example, falling back to the old interactive prompt with a warning, or naming the symlink in the error. The remedy changes product behaviour, so it needs the user's authorization.
  • ℹ️ bin/fm-codex-trust.sh:113 - The trust write uses expectedVersion from a single config/read, and any version conflict is a hard refusal with no retry. The spawn fails at bin/fm-spawn.sh:4549. Two firstmate homes on one host share ~/.codex/config.toml: a primary and a local secondmate dispatching codex workers at the same moment, or a running codex session rewriting its config. In that case one spawn can lose the race and be refused, even though a fresh read-and-write would succeed. The sibling bin/fm-claude-trust.sh re-reads and retries within bounded attempts in the same situation. A bounded re-read and retry on conflict would correct this within scope. However, tests/fm-codex-trust.test.sh (the 'version conflict refuses registration' case) deliberately asserts the refusal, so whether to retry is the user's call.

🔧 Fix applied.
1 warning still open:

  • ⚠️ bin/fm-codex-trust.sh:38 - ownedRegular uses lstat and requires a regular file. As a result, a ${CODEX_HOME:-~/.codex}/config.toml that is a symlink causes the helper to refuse (lines 41, 87, 114 and 122). Because bin/fm-spawn.sh:4549-4552 treats that refusal as fatal, every codex worker, scout and secondmate spawn fails on that host. Symlinks are the normal layout when dotfile managers (stow, chezmoi, home-manager) manage config.toml. Before this change such a host launched codex and showed the trust prompt. Now it cannot launch codex at all, and the error claims the worker 'would wedge on the trust dialog'. Failing closed on symlinked stores is a deliberate, documented choice, and probably protects against Codex's atomic replace overwriting the link. The user should still confirm that blocking every codex spawn is preferred over, for example, falling back to the old interactive prompt with a warning, or naming the symlink in the error. The remedy changes product behaviour, so it needs the user's authorization.
🔧 **Test** - 2 issues found → auto-fixed (2) → no changes applied ✅
  • ℹ️ tests/fm-control-herdr-smoke.test.sh - The Herdr control smoke test fails locally (Herdr 0.9.3) at the 'composer is not proven empty' assertion: the control plane now reports 'composer visibly holds pending text'. Base commit 53b5bc1 fails identically, so this branch didn't cause it. As the verification plan asks, the PR should not claim this test passes until upstream CI reports success.
  • 🚨 live validation verdict: no-go (5 of 7 scenarios were driven live against the product); failed: Herdr control smoke still passes on the private lab session after it switched to tests/lib.sh
  • Live validation: ❌ no-go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
A Codex worker spawned into a fresh worktree reaches the composer without the trust prompt ✅ pass live real-codex-spawn-transcript.txt; live-e2e.log
Only the exact worktree is trusted: primary checkout, ancestor directory, unrelated project and $HOME are refused ✅ pass live adversarial-real-codex.txt
Spawn refuses rather than launching a worker when trust can't be recorded (explicitly untrusted entry, or codex missing) ✅ pass live real-codex-spawn-transcript.txt
Existing Codex config is preserved, a repeat registration changes nothing, and unsafe stores (malformed, symlinked, relative CODEX_HOME) are refused ✅ pass live live-e2e.log; adversarial-real-codex.txt
The trust helper exits even if the Codex server ignores SIGTERM during shutdown ⏸️ untested no The earlier payload did not establish a live result. This was only covered by the suite's stalled-server fixture (fm-codex-trust.log), because a real codex that ignores SIGTERM can't be produced on de…
Test suites spawn codex without a codex install and never write the operator's ~/.codex ⏸️ untested no The earlier payload did not establish a live result. The only evidence was test-suite runs through the private CODEX_HOME and the config-only fixture (suite-results.txt), plus an unchanged SHA for the…
Herdr control smoke still passes on the private lab session after it switched to tests/lib.sh ❌ fail live fm-control-herdr-smoke.log; fm-control-herdr-smoke-BASE.log
  • bash tests/fm-codex-workspace-trust-live-e2e.test.sh (real codex-cli 0.159.3: real config API writes, unregistered vs registered TUI counterfactual)
  • Ran fm-spawn.sh codex ship spawns against the real codex app-server (with fake tmux and treehouse for the pane mechanics), then launched the real codex TUI in a pty in the spawned worktree and in the primary checkout
  • Spawn refusal paths with real codex: worktree marked explicitly untrusted, and no codex on PATH (the CI ENOENT case)
  • Ran bin/fm-codex-trust.sh by hand against the real codex: a linked worktree, then refusal cases (primary checkout, ancestor directory, worktree of an unrelated project, $HOME, untrusted entry, relative CODEX_HOME, symlinked config.toml)
  • bash tests/fm-codex-trust.test.sh (includes the stalled-shutdown SIGKILL regression)
  • bash tests/fm-spawn-worktree-settle.test.sh, tests/fm-secondmate-lifecycle-e2e.test.sh, tests/fm-live-gate.test.sh, tests/fm-test-fixture-cleanup.test.sh
  • bash tests/fm-control-herdr-smoke.test.sh on its private fm-lab-control-smoke-* Herdr session, then the same test from a git archive export of base commit 53b5bc11 for comparison
  • Compared the SHA of the real ~/.codex/config.toml before and after

🔧 No changes applied.
3 issues (2 errors, 1 info) still open:

  • 🚨 bin/fm-remote-entrypoint.sh:139 - The remote-secondmate suites still write the operator's real ~/.codex/config.toml. tests/lib.sh exports a suite-private CODEX_HOME and puts a config-only codex wrapper on PATH. But bin/fm-remote-entrypoint.sh runs unset HOME, resolves the real account home, composes an operator PATH that includes /opt/homebrew/bin, and re-execs the job under env -i. So the remote secondmate spawn's bin/fm-codex-trust.sh uses the real codex and the real store. Observed live: running tests/fm-remote-secondmate-parent-binding.test.sh (exit 0) added a /private/var/folders/.../fm-remote-parent-binding.M2qcPN/remote-home trusted entry. The tester removed that one entry. Three earlier leaked entries (fm-remote-parent-binding.qh2R7x, fm-remote-secondmate-e2e.BQWWQT, fm-remote-trace-context.65MynB) remain in the operator's store for the operator to remove. On CI runners without codex, the same path probably still hits spawn codex ENOENT for the remote suites. A fix needs either a test seam that carries CODEX_HOME and the fixture through the remote job environment, or remote suites that provide a fake account home or codex. Both touch the remote-entrypoint isolation contract, so firstmate should decide.
  • ℹ️ tests/fm-control-herdr-smoke.test.sh - The Herdr control smoke still fails locally (Herdr 0.9.3) at the 'composer is not proven empty' assertion, exactly as on base 53b5bc1. Every earlier check, including the Codex relaunch, now passes. The PR should not claim this test passes until upstream CI reports success.
  • 🚨 live validation verdict: no-go (6 of 6 scenarios were driven live against the product); failed: Test suites that spawn Codex secondmates never write the operator's real ~/.codex store (the isolation rule stated in commit 43a6633)
  • Live validation: ❌ no-go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Fresh Codex worker spawned into a new treehouse worktree starts work without stopping on Codex's 'Trust this folder?' prompt ✅ pass live codex-worker-spawn-no-trust-prompt.txt: spawned trustlive harness=codex; the pane shows the launch brief submitted and the Ask Codex composer, and grep finds 0 trust-dialog markers. (The dummy API…
Trust is registered for the exact launch worktree only (not the treehouse root or the project), and existing Codex config is preserved ✅ pass live Lab config.toml after the spawn keeps # operator comment and model_reasoning_effort and adds only [projects."<exact worktree>"] trust_level = "trusted". The live e2e confirms the unregistered pr…
Adversarial: when trust cannot be recorded (malformed config.toml), the Codex spawn refuses instead of launching a worker that would wedge ✅ pass live codex-worker-spawn-refusal.txt: exit=1 with the 'refusing to launch a codex worker' diagnostic, config.toml unchanged, no codex process in the window, no state/trustrefuse.meta
Real Codex config API: idempotent registration, TOML preservation, malformed input refused, registered worktree reaches the composer while the unregistered checkout is gated ✅ pass live bin/fm-test-run.sh tests/fm-codex-workspace-trust-live-e2e.test.sh → codex-trust-live-e2e.log (exit 0)
Herdr control smoke on a private lab session has no regression from this branch (it previously failed in round 1) ✅ pass live herdr-smoke-head.log and herdr-smoke-base.log: both pass every check, including the stale-registration relaunch, then fail at the same pre-existing 'composer is not proven empty' assertion; HEAD's out…
Test suites that spawn Codex secondmates never write the operator's real ~/.codex store (the isolation rule stated in commit 43a6633) ❌ fail live remote-secondmate-real-codex-store-leak.txt: tests/fm-remote-secondmate-parent-binding.test.sh exited 0, but ~/.codex/config.toml gained a `/private/var/folders/.../fm-remote-parent-binding.M2qcPN/r…
  • HOME=<isolated> FM_CODEX_WORKSPACE_TRUST_LIVE=1 bash bin/fm-test-run.sh tests/fm-codex-workspace-trust-live-e2e.test.sh (real codex-cli 0.159.3 against a disposable CODEX_HOME)
  • Lab home via bin/fm-lab-home.sh create; on a private tmux -L fm-lab socket, real bin/fm-spawn.sh trustlive <lab project> --scout --harness codex --backend tmux with a disposable CODEX_HOME and dummy API key; captured the worker pane and the lab config.toml
  • Same lab: replaced config.toml with malformed TOML, then bin/fm-spawn.sh trustrefuse ... --harness codex; checked for the refusal, an unchanged store, no codex process and no task meta
  • bash tests/fm-control-herdr-smoke.test.sh at HEAD (private fm-lab-control-smoke-* Herdr session)
  • bash tests/fm-control-herdr-smoke.test.sh in a temporary base-commit (53b5bc11) worktree for comparison
  • bash tests/fm-remote-secondmate-parent-binding.test.sh, with shasum of ~/.codex/config.toml taken before and after
  • Teardown: killed the lab tmux server; removed the lab home, the treehouse worktree the spawn created, the base worktree, and the leftover test scratch dirs; removed only the ~/.codex entry the remote suite added during this run

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
A codex worker spawned into a fresh worktree starts working with no 'Trust this folder?' prompt ✅ pass live lab-codex-worker-pane.txt, lab-codex-spawn.log, lab-codex-config-after-spawn.toml
A codex spawn whose trust cannot be registered refuses instead of launching a wedged worker ✅ pass live lab-codex-refused-spawn.log, lab-codex-refused-pane.txt
The real codex TUI shows the trust gate for an unregistered checkout and not for a registered worktree; the native config API preserves TOML ✅ pass live fm-codex-workspace-trust-live-e2e.log
The trust helper exits even when the Codex server ignores SIGTERM during shutdown (Greptile ci-1 fix) ⏸️ untested no The prior payload did not establish a live result. This case ran only against a stalling fixture server in fm-codex-trust.test.sh, because a real codex cannot be made to ignore SIGTERM on demand. A fi…
Remote and local secondmate test suites never write the operator's real ~/.codex/config.toml ✅ pass live real-codex-store-before.sha / real-codex-store-after.sha / real-codex-store-final.sha; fm-remote-secondmate-*.log
On a CI-like runner without codex, suites that spawn codex no longer fail with 'spawn codex ENOENT' ✅ pass live ci-sim-*.log, ci-sim-path.txt
Herdr control smoke passes with the suite-private CODEX_HOME ⏸️ untested no The verification plan makes upstream CI the only verifier for tests/fm-control-herdr-smoke.test.sh and says not to drive Herdr lifecycle locally for this follow-up. Earlier rounds found that local Her…
  • bash tests/fm-codex-workspace-trust-live-e2e.test.sh (real codex 0.159.3: a fresh unregistered checkout shows the trust gate; the registered worktree reaches the composer; TOML preserved; idempotent; malformed store refused)
  • bash tests/fm-codex-trust.test.sh (eligibility, preservation, fail-closed, version conflict, stalled-shutdown SIGKILL, spawn forwards CODEX_HOME, spawn refuses)
  • Lab: bin/fm-lab-home.sh create + private tmux -L fm-lab + FM_SPAWN_NO_GUARD=1 bin/fm-spawn.sh codexlive <lab>/proj --mode local-only --yolo off --harness codex with the real codex and an isolated CODEX_HOME holding a dummy key
  • Lab adversarial: same spawn (codexrefuse) against a malformed config.toml in the lab CODEX_HOME
  • bash tests/fm-remote-secondmate-parent-binding.test.sh, fm-remote-secondmate-trace-context, fm-remote-secondmate-lifecycle-e2e, fm-spawn-compact-adviser-disable-remote, fm-secondmate-lifecycle-e2e with real codex on PATH, and shasum ~/.codex/config.toml before and after
  • bash tests/fm-codex-hook-layer-live-e2e.test.sh, bash tests/fm-harness-liveness-drift-live-e2e.test.sh followed by a real-store hash check
  • Simulated CI PATH (homebrew tools symlinked without codex): fm-secondmate-lifecycle-e2e, fm-spawn-compact-adviser-disable, fm-codex-trust, fm-remote-secondmate-parent-binding

✅ No issues found.

  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Captain spawns a Codex scout into a fresh treehouse worktree and it starts work with no 'Trust this folder?' prompt ✅ pass live live-scout-codex-pane.txt, live-scout-spawn.out: spawn exit 0, the brief is shown as submitted and the 'Ask Codex' composer is up with no trust dialog (the model call returned 401 because of the dummy…
Only the exact launch path is trusted (not the treehouse root, not the primary checkout) ✅ pass live live-lab-codex-store.toml holds exactly the worktree path and the secondmate home path; the existing comment and settings are preserved
Captain spawns a Codex secondmate into a seeded home and it opens without the folder-trust prompt ✅ pass live live-secondmate-spawn.out (exit 0) and live-secondmate-codex-pane.txt: no folder-trust dialog. Codex's separate hook-review dialog appears, which the harness doc says is expected for secondmates
Adversarial: a spawn whose Codex store is malformed refuses before launching and leaves the store as it was ✅ pass live live-refusal-spawn.out shows 'could not pre-register Codex workspace trust ... refusing to launch'; live-refusal-pane.txt shows a shell with no codex; the store still reads 'broken = ['
Real Codex: an unregistered checkout shows the trust gate and a registered worktree reaches the composer; repeat registration is idempotent ✅ pass live bash tests/fm-codex-workspace-trust-live-e2e.test.sh drives real codex 0.159.3 TUI (suite-fm-codex-workspace-trust-live-e2e.log)
Adversarial: a Codex server that stalls on shutdown cannot hang the trust helper (decided ci-1 fix) ⏸️ untested no The previous payload covered this only with a test-fixture suite run (tests/fm-codex-trust.test.sh against a stalling fixture server), not against the live product, so it did not establish a live resu…
Test suites that spawn Codex (local and remote secondmates) never write the operator's ~/.codex store ⏸️ untested no The previous payload supported this with suite runs and a store hash comparison, recorded as not live, so it did not establish a live result.
Herdr control smoke still passes once it uses the suite-private Codex fixture ⏸️ untested no The verification plan and the repo runbook leave Herdr lifecycle to the upstream CI Herdr runner. Earlier rounds found that this smoke test also fails locally on base with Herdr 0.9.3, at the 'compose…
  • Lab: bin/fm-lab-home.sh create $LAB + private tmux -L fm-lab session (FM_HOME=$LAB, CODEX_HOME=$LAB/codex with dummy API key, TREEHOUSE_ROOT=$LAB/treehouse)

  • bin/fm-spawn.sh scout1 $LAB/projects/alpha --scout --harness codex (real codex + real treehouse) then tmux capture-pane of fm-scout1 and read of $LAB/codex/config.toml

  • bin/fm-home-seed.sh mate1 <home> alpha + bin/fm-spawn.sh mate1 <home> --harness codex --secondmate, then pane capture and store read

  • CODEX_HOME=$LAB/codex-broken bin/fm-spawn.sh scout2 ... --scout --harness codex against malformed config.toml (refusal path)

  • bash tests/fm-codex-trust.test.sh

  • bash tests/fm-codex-workspace-trust-live-e2e.test.sh (real codex TUI counterfactual)

  • bash tests/fm-secondmate-harness.test.sh

  • bash tests/fm-remote-secondmate-parent-binding.test.sh

  • bash tests/fm-secondmate-lifecycle-e2e.test.sh

  • shasum -a 256 ~/.codex/config.toml before and after all runs (read-only)

  • ⚠️ bin/fm-codex-trust.sh:135 - Live races against the real codex-cli 0.159.3 app-server show that concurrent registrations into one shared CODEX_HOME mostly collide through a lost update, not a configVersionConflict. config/value/write returns status ok, then another writer's replace drops the entry, and the post-loop read-back refuses with 'Codex configuration did not retain workspace trust'. The bounded retry added for review-2 only catches configVersionConflict, so this path is never retried. Observed: 2 of 25 two-way races and 4 of 12 four-way races refused at least one spawn (race-postcheck-n2.log, race-postcheck-n4.log). This is fail-closed: no helper reported success and then lost its entry. Fix within the recorded decision: move the read-back verification into the bounded attempt loop, so a missing entry after an ok write re-reads and rewrites, up to the same 3 attempts, then refuses. Add a fixture mode where the write succeeds but the next read omits the entry, and assert retry-then-success and retry-past-bound refusal.

  • ℹ️ bin/fm-codex-trust.sh:91 - Several helpers started at once against a never-used CODEX_HOME can fail with 'Codex config server exited (1)', because codex app-server's own sqlite state initialization races ('failed to initialize sqlite state runtime'). This is Codex-side and only affects a cold store; a real ~/.codex is already initialized. The spawn fails closed. For information only.

  • 🚨 live validation verdict: no-go (7 of 7 scenarios were driven live against the product); failed: Adversarial: concurrent codex spawns sharing one Codex store all register via the bounded re-read/retry (user decision review-2)

  • Live validation: ❌ no-go - 7 of 7 scenarios driven live against the product

Scenario Result Live Evidence
Firstmate spawns a codex scout into a fresh treehouse worktree; the worker starts on the brief with no 'Trust this folder?' prompt ✅ pass live lab-spawn-codex-pane.txt, lab-spawn-transcript.txt: spawn exit 0; the pane shows the launch brief submitted and the idle composer, with zero trust-dialog markers
Spawn trusts only the exact launch worktree, never the treehouse root, and keeps existing Codex settings ✅ pass live lab-spawn-transcript.txt: config.toml holds model_reasoning_effort plus one [projects."…/th/.treehouse/proj-679b84/1/proj"] trusted entry; race-and-guards.log: treehouse root refused
Spawn whose trust registration fails refuses before launching codex and does not overwrite the store ✅ pass live lab-spawn-transcript.txt: SPAWN2_EXIT=1, 'could not pre-register Codex workspace trust … refusing to launch a codex worker', store still 'broken = [', pane foreground command is zsh
Registration refuses a primary checkout, an ancestor directory and an explicitly untrusted entry ✅ pass live race-and-guards.log (three refusals, exit 1, untrusted entry kept)
Registered worktree reaches the composer while an unregistered checkout of the same repo still shows the trust gate; repeat registration is a no-op ✅ pass live bash tests/fm-codex-workspace-trust-live-e2e.test.sh → live-e2e.log
Operator's real ~/.codex/config.toml is never written during validation ✅ pass live shasum eb03ae41… before and after every run
Adversarial: concurrent codex spawns sharing one Codex store all register via the bounded re-read/retry (user decision review-2) ❌ fail live race-postcheck-n2.log (2/25 two-way runs refused), race-postcheck-n4.log (4/12 four-way runs refused), race-n2-failures.log: 'Codex configuration did not retain workspace trust'
  • bash tests/fm-codex-workspace-trust-live-e2e.test.sh (real codex-cli 0.159.3: TOML preserved, repeat run is a no-op, the unregistered checkout shows the trust gate, the registered worktree reaches the composer, malformed TOML is refused)
  • bash tests/fm-codex-trust.test.sh (fixture: retry on version conflict clears/persists, stalled-shutdown SIGKILL, secondmate eligibility, spawn registers/refuses)
  • race-and-guards.sh: 8 concurrent real-codex registrations into a cold shared CODEX_HOME, plus primary checkout, treehouse root and explicit-untrusted refusals
  • race-warm-store.sh x3 (N=8 warm store), race-warm-n2.sh/race-warm-n4.sh x5 each, race-postcheck.sh <root> 2 x25 and race-postcheck.sh <root> 4 x12: concurrent real-codex registrations with a post-check that every reported success persisted
  • Lab: bin/fm-lab-home.sh create, tmux -L fm-lab primary shell, bin/fm-spawn.sh scout-trust1 $LAB/projects/proj --scout --harness codex with CODEX_HOME=$LAB/codex and TREEHOUSE_ROOT=$LAB/th; captured the worker pane and the store
  • Lab: same spawn (scout-trust2) with a malformed $LAB/codex/config.toml; checked the exit code, diagnostic, store contents and that codex was not the pane's foreground command
  • shasum ~/.codex/config.toml before and after all runs (unchanged), then lab teardown (tmux -L fm-lab kill-server, fm-lab-home.sh teardown, rm -rf)

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 3 of 5 scenarios driven live against the product
Scenario Result Live Evidence
A Codex worker launched into a fresh linked worktree reaches its composer with no 'Trust this folder?' prompt, while an unregistered checkout still shows the prompt ✅ pass live r6-live-e2e.log (real codex-cli 0.159.3 TUI)
Adversarial: concurrent Codex spawns sharing one store all register through the bounded re-read and retry, including writes lost to a concurrent replace ✅ pass live r6-race-real-codex.log, r6-race-8way.log: 0 refusals and 0 missing entries across 138 registrations; the pre-existing config key was preserved
Primary checkouts, unrelated paths, explicitly untrusted entries, malformed configs and symlinked stores are refused; a spawn whose registration fails refuses before launching ⏸️ untested no The earlier payload did not establish a live result. This scenario was run only through the helper with the config fixture, and through fm-spawn inside the test harness (tests/fm-codex-trust.test.sh).…
A Codex server that stalls on shutdown is killed and the refusal still returns (Greptile P2 decision) ⏸️ untested no The earlier payload did not establish a live result. A real codex can't be made to ignore SIGTERM on demand, so this was checked only with the suite's stalling fixture server in tests/fm-codex-trust.t…
Secondmate spawns in tests find the config-only codex on a minimal PATH and never write the operator's real ~/.codex store ✅ pass live r6-secondmate-harness.log exit 0; checksum of ~/.codex/config.toml unchanged
  • bash tests/fm-codex-trust.test.sh (conflict retry and lost-update retry, each clearing and past the bound; stalled-shutdown SIGKILL; fail-closed stores; spawn refuses before launch)
  • bash tests/fm-codex-workspace-trust-live-e2e.test.sh (real codex TUI: an unregistered checkout stops at the trust prompt, a registered worktree reaches the composer with no input)
  • Real codex app-server race: bin/fm-codex-trust.sh run concurrently on fresh linked worktrees sharing one disposable warmed CODEX_HOME, 2-way x25, 4-way x12 and 8-way x5
  • bash tests/fm-secondmate-harness.test.sh, with a checksum of the operator's ~/.codex/config.toml taken before and after
✅ **Document** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds Codex workspace trust pre-registration to the spawn flow.

The PR appears safe to merge, with a non-blocking remote test isolation issue.

Reviews (4) · Last reviewed commit: "no-mistakes(test): Route remote-suite Co..." · Reviewed by Greptile

Comment thread bin/fm-codex-trust.sh Outdated
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks, @andrewkimcanidium. I read the whole diff of head 95f8cb8d against main 237e1cf3.

What I checked: the new bin/fm-codex-trust.sh runs a private codex app-server over stdio and writes projects."<exact launch dir>".trust_level = "trusted" into ${CODEX_HOME:-~/.codex}/config.toml through Codex's own config/value/write with an expected version, then reads it back. It trusts only an isolated linked worktree of the named project or a seeded secondmate home, never the treehouse root, an ancestor, home or the primary checkout. It refuses an existing untrusted entry, symlinked or foreign-owned stores, malformed config and a relative CODEX_HOME. fm-spawn.sh calls it for every codex worker and secondmate, next to the existing Claude pre-registration. It refuses the spawn if registration fails, and it forwards CODEX_HOME into the launch. Node was already required for the Claude and Agy trust paths, so that isn't a new dependency. The tests use a local stub, and the live e2e only runs when you opt in, with a dummy key. There are no workflow edits, no network calls and no real secrets. Greptile's earlier shutdown concern is handled (SIGKILL after 2 s), and it's 5/5 now. The no-mistakes attestation matches this head.

CI: I approved this head's fork runs 37542141427 (Require no-mistakes) and 37542141459 (CI). Results are pending.

Contract-class: new-default. On main, an unconfigured Codex spawn launches without touching the user's Codex config. With this PR, every Codex worker and secondmate spawn writes a persistent trust entry into the user-global ~/.codex/config.toml with no setting to turn that off, and a new refusal path stops the spawn when that write can't be confirmed. The Claude pre-registration is a precedent for doing this, not an existing promise for Codex, so this adds a default-on behavior rather than restoring one. Kun decides; it won't auto-merge.

Things Kun will weigh (no action needed unless you want to address them): workspace trust is a security-relevant Codex setting, and this grants it automatically on every spawn. Entries also build up in config.toml with no cleanup at teardown. That's bounded by pool slots if paths get reused, but it's still a standing change to the user's harness config.

VISION.md, rule by rule

  • One captain, one interface: aligns. It removes a "Trust this folder?" interruption that would otherwise land on the captain for every new worktree.
  • Authority is explicit and never inferred: cannot tell. Granting harness workspace trust automatically on every spawn is security-sensitive and defaults to on, so it needs Kun's explicit word. It is tightly scoped to the exact launch directory and fails closed.
  • Scripts own the mechanics, agents own the judgment: aligns. The registration is a deterministic script that uses the vendor's own config API.
  • A restart is a non-event: aligns. Trust lives in Codex's durable config, so a relaunch into the same path is a no-op.
  • Delegation with a spine: aligns. Isolation checks stay in place, and the spawn refuses instead of guessing.
  • The fleet outlives any vendor: aligns. It binds to the config API Codex actually exposes rather than reading the TUI, and it brings Codex level with the existing Claude path.
  • Scope: aligns. This is spawn-time setup in the command layer.

Next step: this is waiting on CI. Once CI and Require no-mistakes are green on 95f8cb8d, it goes to Kun for a merge-or-leave decision. That part is on him, not you.

@andrewkimcanidium
andrewkimcanidium force-pushed the fm/fm-codex-trust-preregister branch from 2d26479 to c902397 Compare October 7, 2026 09:09
Comment thread tests/lib.sh
cat > "$script" <<SH
#!/bin/sh
if [ "\${1:-}" = app-server ]; then
FM_TEST_CODEX_STORE='$CODEX_HOME/config.toml' exec '$(command -v node)' '$ROOT/tests/codex-config-fixture.js'

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 Tests touch personal Codex settings

fm_test_remote_codex_config redirects only the fixture's file writes. The remote job clears CODEX_HOME and uses the real account's HOME, so fm-codex-trust.sh still creates ~/.codex and checks the operator's real config.toml before starting the fixture. These portable tests leave a directory outside their temporary folders. If the operator's config is symlinked, the tests can also fail for reasons unrelated to the behavior being tested.

Give the remote trust helper a temporary CODEX_HOME, not just the fixture a different output file.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for the follow-up, @andrewkimcanidium. I re-read the thread and the new head c9023977. It's rebased onto 53b5bc11 and adds one commit since my last pass on 95f8cb8d.

What changed: c9023977 is test-only. A new fm_test_remote_codex_config drops a fake codex into the remote fixture's bin/ that answers app-server with the config fixture and writes to the suite-private store (FM_TEST_CODEX_STORE). The four remote-secondmate suites call it. I diffed bin/fm-codex-trust.sh and bin/fm-spawn.sh between 95f8cb8d and c9023977, and they're byte-identical, so my earlier production-code review still holds. No workflow changes, no network calls, no secrets.

CI: the previous head's CI run (37542141459) came back red, and Require no-mistakes (37542141427) was green. I approved this head's fork runs 37598692066 (CI) and 37598692145 (Require no-mistakes). Results are pending. The no-mistakes attestation matches c9023977, and the PR is MERGEABLE.

One non-blocking note (Greptile P2, "Tests touch personal Codex settings", tests/lib.sh ~323): the remote job still runs under env -i with the real account HOME and no CODEX_HOME. So bin/fm-codex-trust.sh on the remote side can still create ~/.codex and stat the operator's real config.toml before it reaches the fixture. Only the fixture's write is redirected. Giving the remote helper a temporary CODEX_HOME would fully close the leak your own pipeline flagged earlier. It's worth fixing in the same round if CI needs another push anyway.

Contract-class: new-default (same as last pass). On main, an unconfigured Codex spawn launches without touching the user's Codex config. With this PR, every Codex worker and secondmate spawn writes a persistent trust_level = "trusted" entry into the user-global ~/.codex/config.toml with no setting to turn it off, and a new refusal path stops the spawn when that write can't be confirmed. That includes a new floor: node plus a Codex build with the app-server config API.

Security FYI (for Kun, no action needed from you): this grants a security-relevant Codex setting automatically on every spawn. It's tightly scoped to the exact launch directory and fails closed, but the entries accumulate in config.toml with no teardown cleanup.

VISION.md, rule by rule

  • One captain, one interface: aligns. It removes a "Trust this folder?" interruption on every new worktree.
  • Authority is explicit and never inferred: cannot tell. Automatically granting harness workspace trust by default is security-sensitive and needs Kun's explicit word.
  • Scripts own the mechanics, agents own the judgment: aligns. A deterministic script using the vendor's own config API.
  • A restart is a non-event: aligns. Trust lives in Codex's durable config, so a relaunch is a no-op.
  • Delegation with a spine: aligns. Scope checks stay in place, and the spawn refuses instead of guessing.
  • The fleet outlives any vendor: aligns. It binds to an API Codex actually exposes, not the rendered TUI.
  • Scope: aligns. Spawn-time setup in the command layer.

Overlap note for Kun: #6397 ("prevent Codex and Pi folder-trust stalls in pooled worktrees"), #5067 ("register Codex directory trust during authorized intake") and #4432 ("handle fresh workspace trust at startup") go after the same Codex trust prompt. #6763 also edits the Codex launch in bin/fm-spawn.sh and several of the same docs and tests.

Next step: waiting on CI for c9023977. Once CI and Require no-mistakes are green, it goes to Kun for a merge-or-leave decision. That part is on him, not you.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for the new round, @andrewkimcanidium. I re-read the thread and the new head b4188732, which is rebased onto current main b062eb94.

What changed since c9023977: I ran git range-diff. Every earlier commit is unchanged. Your local "isolate Codex config in Herdr control smoke" commit was dropped on rebase, after #6818 reworked that test on main. One new commit, b4188732, is test-only: tests/fm-secondmate-harness.test.sh copies the config-only codex wrapper into its two minimal-PATH spawn fakebins. That fixes the Behavior portable serial 8 failure ("split: no meta written") from the last CI run. bin/fm-codex-trust.sh and the trust call in bin/fm-spawn.sh didn't change, so my earlier production review still holds. No workflow changes, no network calls, no secrets.

CI: Require no-mistakes is green on b4188732, and the attestation matches this head. I approved this head's fork CI run 37741201439 along with 37741201461 (no-mistakes). CI is pending. MERGEABLE.

Contract-class: new-default (unchanged). On main, an unconfigured Codex spawn doesn't touch the user's Codex config. With this PR, every Codex worker and secondmate spawn writes a persistent trust_level = "trusted" entry into the user-global ~/.codex/config.toml, with no setting to turn it off. It also adds a new refusal and version floor (node plus a Codex build with the app-server config API).

Security FYI (for Kun, no action needed from you): this automatically grants a security-relevant Codex setting on every spawn. It's scoped to the exact launch directory and fails closed. The entries do accumulate with no teardown cleanup.

VISION.md, rule by rule

  • One captain, one interface: aligns. It removes the "Trust this folder?" interruption on every new worktree.
  • Authority is explicit and never inferred: cannot tell. Granting harness trust automatically and by default is security-sensitive, so it needs Kun's explicit word.
  • Scripts own the mechanics, agents own the judgment: aligns. It's a deterministic script on the vendor's config API.
  • A restart is a non-event: aligns. Trust is durable, and a relaunch is a no-op.
  • Delegation with a spine: aligns. The scope checks stay, and the spawn refuses instead of guessing.
  • The fleet outlives any vendor: aligns. It binds to an API Codex actually exposes.
  • Scope: aligns. This is spawn-time setup in the command layer.

Overlap note for Kun (unchanged): #6397, #5067 and #4432 target the same Codex trust prompt.

Next step: this is waiting on CI for b4188732. Once CI is green, it goes to Kun for a merge-or-leave decision. That part is on him, not you.

…rust helper could hang if the Codex server stalled while shutting down. The rule that was broken: once the helper has a result (trusted or refused), it must exit within a fixed time, whatever the Codex server does. Both the success path and the refusal path end in one shared `finally` cleanup in `bin/fm-codex-trust.sh`. That cleanup only closed stdin and sent SIGTERM, so a server that ignored SIGTERM kept its process and stdout open. The helper then never exited, and `fm-spawn.sh` waited forever instead of reporting the refusal. **Fix** (in that one cleanup, which covers both paths): after SIGTERM, a 2-second timer sends SIGKILL and closes the server's stdout. The timer is unref'd, so a server that exits normally adds no delay. I updated the help header to mention the 2-second grace and extended the `--help` sed range by one line to match. **Regression test:** - `tests/codex-config-fixture.js` has a new `FM_TEST_CODEX_CONFIG_STALL` mode. The fixture records its pid, ignores SIGTERM and stays alive. - `tests/fm-codex-trust.test.sh` runs a refusal (broken config) against that stalling fixture. It checks that the helper exits within 10 seconds with code 1 and that the stalled server is no longer running. - With the old cleanup the new test fails ("helper hung on a server that stalled during shutdown"); with the fix it passes. **Verification:** - The full `tests/fm-codex-trust.test.sh` passes, about 11 seconds in total. - `fm-spawn-worktree-settle`, `fm-spawn-orca-worktree` and `fm-control-relaunch` pass. - `shellcheck` is clean on both changed shell files. I did not push or re-run validation; the outer executor owns those phases
…tructure problems. I made a fix, but I had not confirmed that the failing suites now pass when this result was required. **Cause.** `bin/fm-spawn.sh` now runs `bin/fm-codex-trust.sh` before every codex spawn, and that helper starts `codex app-server`. CI runners have no `codex` binary. So every suite that spawns a codex worker or secondmate without the fixture got `refusing to pre-register Codex workspace trust: spawn codex ENOENT` and refused to launch. The CI logs show this in fm-secondmate-lifecycle-e2e, fm-control-herdr-smoke, fm-remote-secondmate-parent-binding, fm-remote-secondmate-lifecycle-e2e and fm-spawn-compact-adviser-disable. The other failing suites in those shards are fm-secondmate-harness, fm-secondmate-safety, fm-secondmate-restart, fm-wake-queue, fm-remote-secondmate-trace-context and fm-spawn-compact-adviser-disable-remote; their logs don't show the error text, but they also spawn codex. The same gap leaks on a developer machine that has codex: running the suite locally had written a `/tmp` trust entry into the real `~/.codex/config.toml`. **Rule that was broken.** No spawn under test should need an installed codex or write the operator's real Codex store. The previous rounds fixed only four suites one by one, so I fixed it in `tests/lib.sh`, which every suite loads: - I moved `fm_test_fake_codex_config` from `tests/fixtures.sh` into `tests/lib.sh` without changing it. `fixtures.sh` loads `lib.sh`, so existing callers still find it. - When `lib.sh` loads, it creates a temporary directory per suite and exports `CODEX_HOME` pointing into it. It also puts the config-only `codex` wrapper first on PATH. That wrapper answers `codex app-server` with `tests/codex-config-fixture.js` and passes any other command to the real codex if one exists, otherwise exits 0. - `fm_live_gate`, just before its tool checks, removes that wrapper from PATH and restores the operator's original `CODEX_HOME` (or unsets it). Live guards such as fm-codex-workspace-trust-live-e2e therefore still use the real codex, and in CI a live codex guard still skips because codex is absent. Production code is unchanged. **Verification.** - `shellcheck -x tests/lib.sh tests/fixtures.sh` is clean. - I built a PATH without codex to imitate CI. With the unfixed test library, fm-secondmate-lifecycle-e2e reproduces the exact CI error (`spawn codex ENOENT` and "secondmate spawn failed"). - I had not confirmed any pass with the fix. Two runs were still going when this result was due: - The failing suites: fm-secondmate-lifecycle-e2e, fm-secondmate-harness, fm-secondmate-safety, fm-spawn-compact-adviser-disable, fm-secondmate-restart, fm-remote-secondmate-lifecycle-e2e, fm-remote-secondmate-trace-context, fm-spawn-compact-adviser-disable-remote, fm-codex-trust and fm-control-herdr-smoke. - Suites the new setup could affect: fm-live-gate, fm-test-fixture-cleanup, fm-remote-doctor, fm-bootstrap, fm-spawn-orca-worktree, fm-spawn-worktree-settle and fm-control-relaunch. - An earlier batch on a broken simulated PATH (node missing) can't be used as evidence. On that batch, fm-remote-secondmate-parent-binding passed. - I have not run fm-wake-queue with the fix. **Risk.** Non-live suites now always find a `codex` on PATH, even in CI. A suite that checks behaviour when codex is not installed (for example around fm-remote-doctor) could behave differently; fm-remote-doctor is in the pending run, and the rest of the CI matrix still has to confirm. "Behavior portable serial 2" was cancelled, not failed, and is not part of this round's findings
…R. The failing suite was tests/fm-secondmate-harness.test.sh, with "not ok - split: no meta written". I fixed it in that test file and it now passes locally. Production code is unchanged. **Cause.** Every codex spawn now runs `bin/fm-codex-trust.sh`, which calls `codex app-server`. `tests/lib.sh` puts a config-only codex wrapper first on PATH for this. But this suite runs its spawns with a minimal PATH (fakebin, BLIND_BIN, then `/usr/bin:/bin:/usr/sbin:/sbin`), so the wrapper drops out. The codex secondmate spawn then can't find codex, refuses to launch and writes no meta. I reproduced the same failure locally, since that minimal PATH has no codex on macOS either. **Rule that was broken.** Every spawn under test must find the config-only codex fixture on its PATH. In this suite, the shared place to fix that is the two spawn fakebin builders: `make_noop_tmux` (used by `spawn_secondmate` and the unverified-harness case) and `make_launch_capturing_tmux` (used by `spawn_secondmate_capture`, the cursor case and the crew case). Both already link node in for the Claude trust registration. **Fix.** Each builder now copies `$FM_TEST_CODEX_ROOT/bin/codex` into its fakebin, next to the node link. It is a copy, not a symlink. My first try used a symlink, and it broke a later test: case C9 writes its own probe to `$fakebin/codex` with `cat >`, which wrote through the symlink and overwrote the shared wrapper. That made the later "fallback ... crew-harness codex" check fail. The other fakebin builder, `make_fake_toolchain`, is used by non-spawn cases, which never fail on a missing codex. **Verification.** - Before the fix, `bash tests/fm-secondmate-harness.test.sh` reproduced the CI failure. - After the fix it exits 0 with all 51 checks passing. - `shellcheck -x` on the suite is clean. - The other 27 suites in shard 8 already passed in CI. I did not re-run them
@andrewkimcanidium
andrewkimcanidium force-pushed the fm/fm-codex-trust-preregister branch from b418873 to 3ffebb8 Compare October 10, 2026 03:56
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for round 6, @andrewkimcanidium. I re-read head 3ffebb8a against main fb75c1f9.

Delta since b4188732: it's rebased, and the earlier commits carry the same content. Two new commits bound the trust write to 3 attempts. The first retries on configVersionConflict, and the second re-reads to catch writes lost to a concurrent replace. The read-back still confirms before success. Remote suites now route Codex trust into a suite-private fixture store, which closes the earlier P2 about touching the real ~/.codex. There are no workflow changes, network calls, or secret use. The attestation matches this head. I approved this head's fork runs 38022342191 (CI) and 38022342192 (Require no-mistakes).

Live evidence: 3 of 5 scenarios ran live against real codex-cli 0.159.3: no trust prompt in a fresh worktree, a 138-registration race with 0 lost, and the operator store left byte-identical. The refusal paths and stalled-shutdown kill are fixture-only, which is reasonable.

Contract-class: new-default (unchanged). Every unconfigured Codex spawn now writes a persistent trust_level = "trusted" entry into the user's Codex config and refuses the spawn if it can't.
Security FYI for Kun: this grants a security-relevant Codex setting automatically. It's scoped to the exact launch dir, and entries accumulate with no teardown cleanup.

VISION: One captain: aligns. Authority: cannot tell (default-on trust grant needs Kun's word). Scripts/mechanics: aligns. Restart: aligns. Delegation: aligns (refuses instead of guessing). Vendor: aligns (vendor config API). Scope: aligns.

Next: this is waiting on CI. Once CI and Require no-mistakes are green, it goes to Kun for a merge-or-leave decision. That part is on him, not you. Overlap note for Kun: #6397, #5067, #4432.

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.

2 participants