Blocker 2 (secret at rest on error paths): all staging now lives in ONE
per-run mktemp -d removed by an EXIT/INT/TERM trap; a curl dying rc=7
mid-run (the reviewer's transport-failure case) leaves nothing behind.
M10 pins it against a dying mock in an isolated TMPDIR; trap-removed
mutant killed.
Blocker 1 (bash -x trace channel, upheld above landed parity because
this is the admin-token minter): secrets are assembled FILE-TO-FILE —
stage_auth/stage_user take token/password FILE PATHS and build the curl
configs with jq --rawfile; the password is generated straight into its
staging file; bodies are composed by jq from the template + password
file. No secret is ever expanded into a shell word a trace would print.
M11 runs a real bash -x and asserts the admin-token value, the minted
token value, and any password-shaped 32-char expansion are all absent;
expansion mutant killed (measured: the mutant's trace shows
'+ PW_VALUE=<32 chars>', the fixed script's trace shows paths only).
Header comment corrected to state what is actually true, including the
explicit note that detect-platform's gitea_write_auth_config still
leaks under -x — that parity gap is now tracked as #1369, opened per
review 263 and fred's ruling; issue-comment/pr-review/pr-edit left
untouched in this PR.
Blocker (rev-security-01 review 259): admin token, generated password and
minted seat token travelled curl ARGV (three Authorization sites, -u at
the mint, inline -d bodies), leaking to /proc cmdline and bash -x traces
— the durable password under must_change_password:false is a live
credential. All three now travel in 0600 staging files: --config for the
Authorization header (the landed gitea_write_auth_config pattern), a
user= directive for basic auth at the mint, --data @file for bodies.
Unlinked after each use; M6 asserts no call is unauthenticated and no
body is inline.
Scope pin (SF2 + rev-security-01 M1, same defect): a mutant writing the
REQUESTED scopes passed green because the grep target appears in both
sets. M7 now asserts write:issue (requested, not granted) is ABSENT;
mutant killed.
SF3: hyphenated instance overrides map hyphen->underscore exactly like
seat-logins.sh (url_override_var); M8 pins it; the uppercase-only mutant
dies at the invalid-variable-name refusal again, now by design.
SF1: mint-seat-credential.sh mode 755 (update-index), README invocation
updated to name the now-required MOSAIC_SEAT_EMAIL_DOMAIN.
Framework-PR firewall answer (rev-security-01): the email domain has NO
default — unset is rc=3 with a named variable (M9); the instance host
map stays per the seat-logins.sh precedent already on next. Estate
domains belong to the estate, not the tree.
Moves seat credential minting out of a brain-local fleet/bin into
packages/mosaic/framework/tools/fleet/, parameterized for any deployment:
- MOSAIC_ADMIN_SEAT (or --admin-seat) names the seat whose admin token calls
the Gitea admin API; no seat name is hardcoded.
- MOSAIC_GITEA_INSTANCES / MOSAIC_GITEA_URL_<INSTANCE> select and override
instances, the same convention seat-logins.sh already uses.
- MOSAIC_SEAT_EMAIL_DOMAIN sets the account email domain.
- tea projection calls the sibling seat-logins.sh, not a brain-local copy.
Hermetic suite test-mint-seat-credential.sh (mock curl, sandboxed brain home,
no tea, no network) pins: slot written from the mint response at mode 600;
admin seat must be configured (rc=3) and its token present (rc=1, no API
call); instance selection and URL override; admin token value never echoed.
Joins ci.yml and the verify-release canonical list. Adds tools/fleet/README.md.
Plan: docs/plans/2026-08-21_git-operations-toolkit.md step 6 (first of three
scripts; new-seat.sh and launch-seat.sh need a design ruling, see the plan).
Review finding from scooby. This case does not use run_start, so
install_pane_binaries' symlinks land under a home its launcher never consults
(HOME is the trusted parent here). It resolves mosaic and pi through
MOSAIC_RUNTIME_BIN=$FAKE_BIN instead. Valid path, valid green — and a trap for
anyone who later drops that env var believing the symlinks cover it, which
would break the #1241 binary check rather than exercise it.
Comment only; no behavior change. Harness rc=0.
Refs #1241.
Review finding from scooby: this PR added a failure branch the harness
structurally could not reach. The fake tmux answered `has-session` only for
`=_holder:0.0`, so every non-holder agent landed in the session-is-gone branch
no matter what — the `elif` (tmux still reports the session, no pane PID after
the retries) had zero coverage and no way to get any.
That is the same shape as the bug this PR exists to fix, one layer down: a code
path shipped green where the gate that should measure it cannot. Less severe,
because the branch fails closed at exit 69 rather than reporting success — but
"the harness can't reach it" is the sentence that precedes the next silent
regression, so it gets closed here rather than filed.
`MOSAIC_TEST_HELD_SESSIONS` lets a case name targets the shim should also
answer for. It answers them only AFTER `new-session`, and that detail is the
whole trick: the launcher asks `has-session` about the same name twice — once
at line 255 where a yes means "already running, exit 0", and once at 417 where
a yes means "the session survived". A shim answering yes to both short-circuits
at the first and never reaches the branch under test. It would have looked like
coverage while measuring the idempotency path.
Both failure modes were measured, not reasoned about:
- toggle absent (the old shim): `code=pane-did-not-survive` — the case lands on
the wrong branch, which is exactly the unreachability being reported.
- toggle answering unconditionally: launcher exits 0 via the idempotency
short-circuit — "launcher reported success over a session with no resolvable
pane PID".
- toggle gated on new-session: `code=pane-pid-unresolved`, exit 69.
The case also asserts the diagnostic is not `pane-did-not-survive` and does not
mention the heartbeat, so the two pane faults cannot collapse into one message.
Gates: bash -n · launcher harness rc=0 · test-fleet-units.sh (real tmux) rc=0 ·
fleet specs 342 passed.
Refs #1241.
`mosaic fleet start` returned 0 over three dead panes. The launcher knew,
and said the wrong thing at the wrong severity to the wrong layer.
The pane runs `mosaic yolo <runtime>` under PANE_PATH with a cleared
environment. When that binary is absent the pane dies in under a second,
tmux destroys the session, and the diagnostic goes with it. The launcher
then found no PANE_PID, printed a WARNING about the *heartbeat sidecar*,
and exited 0 — so systemd logged "Finished ... successfully" and
`fleet start` reported success. `fleet ps` was the only component telling
the truth.
Two changes, both in start-agent-session.sh:
1. Before any effect, resolve `mosaic` and the roster's runtime against
PANE_PATH — the pane's own view of the path, not the launcher's.
`mosaic yolo <runtime>` calls checkRuntime(runtime) and looks for a
binary named exactly like the runtime, so this asks the same question
the pane will ask a moment later, while an operator can still see the
answer. Absent binary -> exit 69, code=missing-binary, no session
created.
2. Replace the dead-pane WARNING+exit-0. An absent session one second
after new-session is a runtime that died on startup, not a heartbeat
problem -> exit 69, code=pane-did-not-survive, with the command to run
by hand to see why. A present session with no pane PID after five
attempts -> code=pane-pid-unresolved. Neither branch kills the
session; destroying a possibly-live pane on a guess is worse than
leaving it for inspection.
Exit 69 (EX_UNAVAILABLE) is deliberate: the 64s already in this file mean
the projection was bad, and here the data is fine and the host is not
ready. Callers separate the cases by `code=`, the same way fail_env's
codes share 64.
This propagates for free. `fleet start` calls runChecked() for the holder
and each agent, and runChecked throws on non-zero, so layers 4 and 5 stop
lying without a TypeScript change. Two adjacent defects are left for a
follow-up issue rather than widened into this diff: the per-agent loop
aborts on the first failure instead of attempting all and reporting an
aggregate, and runChecked's bare throw surfaces the launcher's message
under a Node unhandled-rejection stack trace because program.parse() is
synchronous.
Tests:
- test-start-agent-session.sh gains three cases: `mosaic` absent from the
pane path, the runtime absent from the pane path, and a pane that does
not survive. Each was verified individually red against the unmodified
origin/next launcher.
- The two cases asserting a valid launch now supply a pane PID. Until now
the suite's one success path was itself a dead pane the launcher
reported as fine.
- The harness fakes `npm` so PANE_PATH stops depending on whatever the
host has installed, and fails loudly if the host provides `mosaic` or
`pi` in the system path, where the missing-binary cases would not be
measurable at all.
- test-fleet-units.sh gains a `pi` shim in its runtime bin. The real-tmux
harness named `pi` in its roster and never installed it; the new
preflight caught it.
Refs #1241