fix(bin): bind each second mate to its own Claude account store - #2902
Open
mdn0420 wants to merge 5 commits into
Open
fix(bin): bind each second mate to its own Claude account store#2902mdn0420 wants to merge 5 commits into
mdn0420 wants to merge 5 commits into
Conversation
Confidence Score: 4/5The PR does not yet appear safe to merge because mutable launch state can change after relaunch preflight and cause a healthy secondmate to be stopped before its replacement is refused. The relaunch preflight and replacement spawn independently validate mutable registry and store state on opposite sides of the stop, with no shared snapshot or lock spanning those checks, so the previously reported race remains. Files Needing Attention: bin/fm-control.sh, bin/fm-spawn.sh Reviews (5): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile |
A --secondmate launch prefixed `claude` with whatever CLAUDE_CONFIG_DIR happened to be set in fm-spawn's own environment, so the account a second mate ran under was ambient rather than recorded. The session-start liveness sweep relaunches a dead second mate with a bare `fm-spawn.sh <id> --secondmate` from the primary's session, which silently moved a mate started under another store onto the primary's, with no visible difference in the pane and no diagnostic. The launch line begins with `env`, so a shell-alias guard against mixing accounts never protected this path. Record the store as an optional `claude-config-dir:` field in that second mate's own data/secondmates.md entry, and re-resolve it on every launch and relaunch exactly as the harness pin is re-resolved, so the account a mate and its own workers authenticate and bill against is durable. The binding is per mate, not fleet-wide: one global setting could not express two mates on two accounts. Resolution fails closed. A recorded store that is not an existing directory, a spawn whose harness resolves to anything but claude, and a remote route each refuse the launch naming the concrete gap, because falling back to the ambient store is the exact drift the binding exists to prevent. A remote second mate's account lives on its own host, out of reach of this launch prefix, so the field is refused there rather than half-applied. An entry without the field parses and behaves exactly as before, inheriting the ambient store, and an unset binding writes the record byte-identically to before the field existed, so no existing registry line needs rewriting. Crewmate and scout launches are untouched: ambient inheritance is correct there, since those workers work alongside firstmate on firstmate's account. fm-home-seed.sh records the binding from FM_SECONDMATE_CLAUDE_CONFIG_DIR, validating it before any mutation so a bad value rolls the seed back, and `validate` covers the new field. fm-remote-home-seed.sh refuses the variable outright. fm-fleet-snapshot.sh's independent registry filter is updated so bound records still resolve there.
…he stop bin/fm-spawn.sh refuses to launch a second mate whose registry entry records a claude-config-dir onto a harness with no Claude store to bind, and refuses a recorded store that is not an existing directory. Both refusals are correct and both are fail-closed, but the launch owner is reached during a relaunch only AFTER the running agent has been stopped. On that path the ordering was the defect. Relaunching a bound second mate onto an incompatible harness stopped its healthy agent, then refused the replacement launch, and the exited|launching rollback left the mate down with no agent at all. Automated recovery then repeated the identical refusal, so it never healed. bin/fm-control.sh already keeps the launch owner's task-kind capability question on the pre-stop side of the transaction for exactly this reason. Ask the two account questions in the same preflight, so an incompatible harness or a missing store is rejected while the current agent is still running and nothing has changed. The spawn-time refusals stay as the fail-closed backstop for a direct spawn, which reaches them with no agent to lose. Regression coverage proves a healthy bound second mate survives both refusals: its agent is still running afterwards, no transaction journal is opened, and its durable record still names the recorded harness. Both tests fail without this change, on the agent being killed. A bound mate whose store is present still relaunches normally, so the preflight refuses only what the launch owner refuses.
mdn0420
force-pushed
the
fm/sm-account-pin
branch
from
August 23, 2026 22:26
0f1a6f4 to
0d23682
Compare
…ner's own validation The pre-stop preflight looked the binding up with a bare registry field read and treated any failure as "this mate records no store". That lookup fails for every unusable registry: a malformed entry, the same id registered twice, an id that does not parse, and a registry that is a symlink or unreadable. The launch owner refuses all of them - it validates the whole registry through secondmate_registry_validate_bindings before it resolves anything. So the two sides disagreed, and the disagreement reopened the outage this fix exists to close, just through a narrower door. A malformed or duplicated registry waved straight past the preflight, the healthy agent was stopped, and the spawn then refused, leaving the second mate with no agent at all and automated recovery repeating the same refusal. Resolve the binding here the way the launch owner resolves it: same trigger (a registry present in any form), same validation, same expected id and home, and fail closed on its error naming the concrete registry problem. A bare field read cannot tell "records no store" from "this registry is unusable", and only one of those may proceed to a stop. Both new regressions reproduce the outage without this change - the agent is stopped and the rollback reports no agent running - and refuse before the stop with it. The unbound and satisfied-binding paths are unchanged, so the preflight still refuses exactly what the launch owner refuses and nothing more.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Bind each second mate's home to a durable Claude account (config store), so a restart cannot silently change which account it and its workers authenticate and bill against.
VERIFIED PROBLEM. bin/fm-spawn.sh prefixed the claude launch line with whatever CLAUDE_CONFIG_DIR happened to be set in fm-spawn's OWN environment at the moment it ran. For a --secondmate launch that made the account a second mate runs under ambient, not recorded anywhere. The automatic relaunch path in bin/fm-bootstrap.sh calls
bin/fm-spawn.sh <id> --secondmatebare, from the primary firstmate's session, so a second mate deliberately started under a different store came back under the PRIMARY's store after any recovery relaunch, with no visible difference in the pane and no diagnostic. The launch line begins withenv, which skips shell alias expansion and resolves the real binary on PATH, so any shell-alias guard the operator has against mixing accounts did not protect this path at all.WHY IT MATTERS. The operator runs two Claude accounts selected by shell aliases: ~/.claude (personal) and ~/.claude-utm (a client's team account). A second mate is about to be created for that client's domain, and its work must never authenticate or bill against the personal account. This binding is the precondition for creating it.
DESIGN, DECIDED IN ADVANCE BY THE USER AND IMPLEMENTED AS SPECIFIED.
ACCEPTANCE CRITERIA THE USER SET.
CONSTRAINTS THE USER SET.
DECISIONS AND TRADEOFFS MADE WHILE IMPLEMENTING, which a reviewer reading only the diff would not know.
addedin BOTH the local and remote line forms. A non-participating optional group still occupies its BASH_REMATCH index and yields an empty string, so the surrounding capture indices stay fixed whether or not the field matched; this is what preserves byte-identical parsing of existing fieldless lines. Adding the group to the remote form as well is what allows the remote refusal to produce a precise diagnostic instead of a generic "malformed entry".VERIFICATION ALREADY RUN LOCALLY: bin/fm-lint.sh clean (shellcheck 0.11.0 and actionlint 1.7.12, both pinned), bin/fm-doc-audience-check.sh ok, bin/fm-test-run.sh --check-coverage ok, and bin/fm-test-run.sh --changed green at 66 scripts with 0 failures (2 gate skips are pre-existing environment gaps for an uninstalled Pi package, unrelated to this change).
What Changed
claude-config-dir:field to thedata/secondmates.mdregistry (local routes only), parsed bybin/fm-secondmate-registry-lib.shand the second jq-based parser inbin/fm-fleet-snapshot.sh, with fieldless entries parsing byte-identically to before.bin/fm-spawn.shresolves this field for--secondmatelaunches and uses it in place of the ambientCLAUDE_CONFIG_DIR, so a relaunch (including through the bootstrap liveness/recovery path) re-resolves and honors the bound store instead of inheriting whatever account the primary firstmate happens to be running under. Launch refuses with a diagnostic if the named store directory doesn't exist, if the resolved harness isn'tclaude, or if the record is a remote route.bin/fm-home-seed.shgains support for recording the binding at seed time (validating the path is absolute and exists, with rollback on failure), and itsvalidatesubcommand checks the new field;bin/fm-remote-home-seed.shexplicitly refuses the corresponding remote env var.tests/fm-secondmate-harness.test.sh,tests/fm-secondmate-liveness.test.sh, andtests/fm-secondmate-safety.test.shwith coverage for bound-launch, backward compatibility, missing-store refusal, non-claude-harness refusal, multi-mate isolation, remote refusal, and the bootstrap-relaunch regression.docs/configuration.mdand.agents/skills/secondmate-provisioning/SKILL.mdto document the registry field, seed behavior, and recovery contract.Risk Assessment
✅ Low: The change is well-bounded, thoroughly tested against real observable behavior (captured launch commands, not source-text matching), fails closed on every documented edge case (missing store, non-claude harness, remote route), preserves backward compatibility via a verified BASH_REMATCH index calculation, and matches every acceptance criterion and design decision stated in the user intent with no contradictions found.
Testing
Targeted test suites (harness, liveness, safety) all pass, exercising every numbered acceptance criterion end-to-end via real fm-spawn.sh launch-line inspection rather than source-text matching, including the specific bootstrap-relaunch-under-a-different-ambient-account regression the intent calls out; docs were confirmed updated at their named owners; no findings.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
./bin/fm-test-run.sh tests/fm-secondmate-harness.test.sh tests/fm-secondmate-liveness.test.sh tests/fm-secondmate-safety.test.sh - 3/3 suites pass, 0 failuresInspected tests/fm-secondmate-harness.test.sh D1-D6 cases directly to confirm they call fm-spawn.sh --secondmate and assert on the captured launch line (real behavior), not a source-text grepgit diff 505c819..0f1a6f4 -- docs/configuration.md .agents/skills/secondmate-provisioning/SKILL.md - confirmed doc updates land at the specified ownersgit status --short - confirmed a clean working tree after testing, no cleanup needed✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.