feat(worker): authorized task home for compartment child requests - #278
Merged
Conversation
… registry canonically
…durable task home
This was referenced Aug 20, 2026
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.
Unblocks the R2/R3 compartment child lane: a secondmate compartment running in Azure can now actually obtain a crewmate. Built to
R2R3-DESIGN.mdAMENDMENT 2 section 1, which rewrote B.5 step 3.The established diagnosis
The controller was never the blocker.
--owner-kindis a plain argv flag onrequest(bin/fm-worker-lifecycle.py, therequestsubparser) and the controller never inspectsFM_HOMEto decide it: a request minted withFM_HOMEon the controller's own home plus--owner-kind secondmateplus the parent pair is admitted with all four B.1 bounds enforced andchildren_totalincremented.The pinch point is that
FM_HOMEdoes three separable jobs at once:authoritative_request_bindingsreadenv["home"]/state/<task>.meta;home_binding;Jobs 1 and 2 belong to the requester, job 3 belongs to the controller, and the compartment child is the first case where they differ. Moving
FM_HOMEto the secondmate home therefore aims at a SECOND controller document and refuses atverify_state's home fence. That fence is not weakened here; it is left byte-unchanged and now has its own golden.What this builds
bin/fm-worker-lifecycle.pyrequestgains--task-home(defaultNone).authoritative_request_bindings(env, task, generation, task_home=None):origin = task_home or env["home"], the metadata read becomesorigin/"state"/(task + ".meta"), and the stamped binding becomeshome_binding(origin). Every other line is unchanged.command_requestaccepts--task-homeonly with a complete parent pair,--role authorand--owner-kind secondmate; otherwise it refuses with the exact stringtask home is owned by compartment child requests only.authorize_task_home(env, state, item, task_home), called INSIDE the existingcontroller_lockhold, immediately before the unchangedenforce_child_bounds.bin/fm-spawn.shTASK_HOME(default$FM_HOME), set from the newFM_SPAWN_TASK_HOME.STATE/DATA/PROJECTSderive from$TASK_HOME;FM_HOME,CONFIG, the lifecycle invocation and the newPRIMARY_STATEstay on the primary.owner_kindderives from$TASK_HOME/$SUB_HOME_MARKERrather than$FM_HOME's marker.--task-home "$TASK_HOME"is forwarded only when it differs from$FM_HOME, so every ordinary spawn keeps its exact pre-flag argv.spawn_cloud_assignment_generation(re-anchored: it is in the cloud placement helpers block, previously reading$STATE/azure-workers/controller.json) now reads$PRIMARY_STATE/azure-workers/controller.json. Without this the child would be reported durably queued forever.$STATEagainst$TASK_HOME/staterather than$FM_HOME/state(it would otherwise refuse every compartment-child spawn outright), andbin/fm-spawn-cloud-monitor.shnow derivesCONTROLLERfrom$FM_HOMErather than from$STATE.FM_SPAWN_TASK_HOMEis refused when it is relative, is not an existing directory, is not a seeded secondmate home, or is combined withFM_STATE_OVERRIDE/FM_DATA_OVERRIDE/FM_PROJECTS_OVERRIDE. When it differs from$FM_HOMEthe spawn additionally takes that home's own lifecycle lock (a dedicated variable, so the--secondmatespawn lane's lock is untouched) and re-validates its marker under the hold.On PR #273. #273 removed the
FM_AZURE_WORKER_STATE_DIRpin, stopped movingFM_HOMEin the compartment monitor, and introduced--spawn-homeonfm-secondmate-cloud-monitor.py child-relay, documented as "the home the child spawn runs under, which is the controller's own home". That concept is correct and is extended rather than duplicated:--spawn-homestays the primary and remains what the spawn runs under, and the compartment's own home (already carried by that lane as--home) is what must now reach the spawn asFM_SPAWN_TASK_HOME. Nothing in #273 is undone, and no parallel concept is introduced. The monitor files themselves are untouched here (a sibling change owns them), so the monitor-side wiring ofFM_SPAWN_TASK_HOMEis the one remaining step; the controller and spawn contract it needs is complete and proven end to end below.The authority chain, and why nothing is self-authorizing
authorize_task_homeruns under the one exclusive hold over the one document, before the bounds it anchors. Three independent links, all owned by the primary:task_home/.fm-secondmate-homemust be a regular non-symlink file whose contents equalitem["parent_task"](the same marker file and shapevalidate_secondmate_homealready validates elsewhere);env["home"]/data/secondmates.mdmust map that secondmate id to exactly that resolved directory - an entry only the primary could have written;enforce_child_boundsmust then find that id as an assignedrole=secondmateentry in this controller's document.A directory that plants its own marker fails link 2. A registry entry naming an unmarked or symlink-marked home fails link 1. A marked and registered home whose secondmate is not a live compartment here fails link 3. No link can be satisfied by the task home alone.
Zero-diff proof
verify_state,empty_stateand the whole C2 claim/lease/fence discipline are byte-unchanged. Proven by AST-slicing bothorigin/main's and this head'sbin/fm-worker-lifecycle.pyand comparing each function's source text:covering
verify_state,empty_state,claim_pending,apply_pending,drain_pending,controller_lock,load_state,save_state,command_abandon_claim,provider_mutate,slot_lease,ensure_unique_bindings,enforce_child_bounds,capacity_admission,outstanding_cost_reservations,home_binding,request_key,release_receipt,verify_release_against_worker,proof_template,command_release,command_surrender.CORRECTION, stated here rather than left to a reader who stops at this section: an earlier revision of this block claimed 18 of 18 with
CHANGED: noneand listedverify_requestamong the byte-unchanged functions. That is no longer true at this head.verify_requestCHANGED: it gained thetask_homeshape guard (the request must carry a complete parent pair,owner_kind=secondmate, no--role secondmate, and ahome_bindingmatching the task home).create_worker_recordandcommand_requestchanged to carrytask_homeadditively. All three changes are strengthenings, and the ordinary lane's emitted bytes are pinned unchanged bylocal_secondmate_lane_bytes_unchanged. The zero-diff claim is the foundation the whole review discipline rests on, so it is restated accurately rather than quietly widened.Test evidence
All hermetic, run as
python3 tests/run-one.py bash tests/<suite>.test.sh(the process reaper lives only inrun-one.py).fm-worker-lifecycle.test.shfm-spawn-cloud.test.shNew units:
compartment_child_task_home- real minted bindings with the meta under a--task-homesecondmate home and the controller under the primary: child ADMITTED,home_bindingequal to sha256 of the secondmate home and different from the document's own, account/worktree/repository bindings recomputed independently,children_totalincremented exactly once. Then: a home the registry names but whose marker names a different secondmate refuses with no queue mutation and no lifetime spend; a marker naming a different secondmate refuses; a home the primary never registered refuses; a secondmate absent from the registry refuses; a missing registry refuses; a marked and registered home whose secondmate is not assigned hits the unchangedenforce_child_boundsstring;--task-homewith no pair, with half a pair, with--owner-kind primaryand with--role secondmateall refuse with the exact string; a symlinked marker refuses. Plus a structural pin that the authorization call sits inside thecontroller_lockhold and beforeenforce_child_bounds.local_secondmate_lane_bytes_unchanged- the AMENDMENT 1 lane (FM_HOME= secondmate home, no parent pair, no--task-home): the emitted queue item is compared byte for byte against an item the test builds from first principles, every digest recomputed in the test. Pinned by recomputation rather than by diffing main's binary because most CI jobs check out shallow, and a golden that needed git history would be a conditional skip dressed as coverage.verify_state_home_fence_golden- AST golden pinningverify_state's exact six-field identity tuple("schema", "home_binding", "subscription_binding", "deployment_generation", "owner", "prefix"), plus the live refusal when a document'shome_bindingis tampered with.test_compartment_child_spawn_splits_the_task_home_from_the_money_document- end to end through the realbin/fm-spawn.sh: a compartment stood up in the primary's controller, then a child spawn withFM_HOME= primary andFM_SPAWN_TASK_HOME= secondmate home. The child's task metadata lands in the secondmate home and NOT in the primary; the primary's document carries the child withowner_kind=secondmate, the parent pair,home_binding= the secondmate home;children_totalis 1; and no secondcontroller.jsonis ever created under the secondmate home.test_task_home_refusals_are_exact- relative path, missing directory, unmarked directory, and the state-override combination all refuse before anything is written.Adjacent suites, all rc=0 and unchanged:
fm-spawn-backlog(7),fm-spawn-batch(3),fm-spawn-dispatch-profile(20),fm-spawn-provision(63),fm-secondmate-cloud-monitor(37),fm-secondmate-liveness(16),fm-secondmate-harness(25),fm-account-directory(35),fm-bootstrap(27),fm-auto-reap(20).bin/fm-lint.shrc=0 over all 246 scripts;bin/fm-lint-node.shrc=0.Mutation proofs
Each mutation applied to the call site, suite run, red recorded, mutation reverted, tree confirmed clean.
authorize_task_homeAssertionError: queued child-2b generation gen-c2b for one isolated author worker- a home the registry names, marked for a different secondmate, is ADMITTEDauthorize_task_homeAssertionError: queued child-3 generation gen-c3 for one isolated author worker- a home carrying a planted marker is ADMITTED--task-homewithout the parent pairAssertionError: queued child-7 generation gen-c7 for one isolated author workerauthorize_task_homeoutside the lockAssertionError: authorize_task_home is not called inside the controller lock holdHonest limits, and what a future wrong edit breaks
--task-homeis ever accepted without the complete parent pair, or the marker check is dropped, or the registry check is dropped, any directory on the box could mint requests against the primary's money document with arbitrary account and worktree bindings. That is the whole exposure of this change, it is why all three checks are separately mutation-proven, and it is why the refusal is a single exact string rather than a family of near-misses.data/secondmates.mdcan authorize any directory it likes. The registry has always been primary-owned, and this change does not raise or lower that.CONFIGdeliberately stays on$FM_HOMErather than moving to$TASK_HOME, per the spec's explicit list ofSTATE/DATA/PROJECTS. That means the child's harness/dispatch configuration is the primary's, which is what the lane does today; a future decision to move it is a separate change.fm-secondmate-cloud-monitor.sh/.pypassingFM_SPAWN_TASK_HOME) is NOT in this PR - a sibling change owns those files. Until it lands, the capability exists and is proven end to end throughbin/fm-spawn.sh, but the production relay still spawns withTASK_HOME == FM_HOMEand its child request is refused atverify_requestfor being primary-owned while carrying a parent pair.docs/azure-workers.mdhad two stale claims about this lane (thatFM_AZURE_WORKER_STATE_DIRstays pinned whileFM_HOMEmoves, and that compartment children are blocked pending an assertable owner kind and an authorized task home). Both are corrected here rather than left contradicting the shipped code.Review round 2 (adversarial review findings 1-6, all addressed)
1 (BLOCKER, fixed) - the capability admitted a child that could never be released
Correct and worse than my original limits section said.
command_authority_receiptandordinary_authority_attemptboth invoked the authority tool with--home str(env["home"]), the PRIMARY, andfm-worker-authority.pyreadshome/state/<task>.meta. The compartment child's meta lands ONLY under the task home, so an admitted child held a live worker slot with no ordinary exit; and because the resulting error carries the literalWORKER AUTHORITY REFUSED, an architectural home mismatch read as a genuine refusal and would have qualified every compartment child for surrender from the moment it was assigned, which is exactly whatordinary_authority_attempt's own comment says must never happen.Fixed by carrying the PATH, not only the digest:
task_home(compartment children only), validated byverify_requestand part ofidentity_fields;create_worker_recordcarries it onto the worker record, additively, so an ordinary worker record keeps its exact bytes andordinary_authority_attempt(which sees only the worker) can find it;authority_home(env, record)returns the task's own home, FENCED on the recordedhome_binding, so a hand-edited path cannot redirect the authority lane;str(authority_home(env, worker)).New unit
compartment_child_reaches_its_ordinary_exitdrives the whole exit for real against a full authority fixture (tmux shim so the endpoint proves absent,completion.mdwith the ordered headings in the TASK home, a clean worktree cloned from a real origin so landing proves reachability, and an account under the real account-directory helper): the child is admitted, assigned, mints all fiveprovedreceipts, and RELEASES through the unchanged release path. It then tamperstask_hometo a decoy home that would otherwise succeed and requires the fence to refuse.2 (BLOCKER, fixed) - the registry parser was a reimplementation that dropped every safety check
Correct.
authorize_task_homeis now the STRICTEST reader ofdata/secondmates.md, not the most permissive: the regex is deleted and the function shells out to the canonicalfm_secondmate_registry_queryinbin/fm-account-routing-lib.sh, inheriting the whole-registry refusal, the absolute-home requirement, the..refusal, the per-componentlstatsymlink refusal, the existing-directory check, and the duplicate-id / duplicate-home-by-inode checks.--task-homemust additionally be absolute at the CLI, so a relative path can never resolve against the request process's cwd.New unit
task_home_registry_is_read_by_the_canonical_readerruns all six shapes, and for each one it FIRST proves the canonical reader itself rejects that registry (so the case cannot pass vacuously), then requires the request to refuse. It runs the relative-home shape with cwd set to the directory it would resolve against, so a cwd-anchored reader genuinely admits there rather than missing by luck. It ends by proving a valid registry still admits.3 (MEDIUM, fixed) - the marker link was weaker than validate_secondmate_home
Correct on both proven admits.
authorize_task_homenow applies the home-shape rules: the task home may not be, contain, or sit inside the active firstmate home, nor be, contain, or sit inside the firstmate repository, and it must carryAGENTS.mdandbin/. The marker comparison strips trailing newlines only (rstrip("\n")), exactly what the shell readers'$(cat ...)does, so a leading-whitespace marker is no longer the secondmate id. The docstring now states precisely which rules are applied rather than claiming a match it did not have.4 (MEDIUM, fixed) - the binding mint ran before the authorization
Correct.
authorize_task_homenow runs BEFOREauthoritative_request_bindings, so nothing reads<task_home>/state/<task>.meta, resolves caller-named worktree and account paths, or runsgit -C <caller path>under a directory the primary has not authorized. The authoritative decision is still taken again inside the lock, immediately beforeenforce_child_bounds; the structural pin now requires both (one call inside the hold before the bounds, and a call before the mint).5 (LOW, fixed) - dead parameter and raw marker bytes in operator output
The unused
stateparameter is dropped (the reviewer allowed either), and the function now cross-checksitem["home_binding"] == home_binding(task_home)under the lock, which closes the gated asserted-bindings gap where--task-homewas authorized and charged againstchildren_totalbut then ignored for the bindings. Refusal text renders the marker throughsafe_marker_text: non-printable bytes and quotes become?, and the value is truncated at 64 characters. A unit plants a marker containing\x07and an ANSI escape and requires neither to appear in the refusal.6 (documentation, fixed) - the third re-anchor is not a pure equivalence
Correct, and now stated plainly:
bin/fm-spawn-cloud-monitor.shderivingCONTROLLERfrom$FM_HOMErather than$STATECHANGES WHATFM_STATE_OVERRIDEMEANS for that script.FM_STATE_OVERRIDEstill redirects where the task's own files are read, but it no longer redirects which controller document is consulted. It is behaviour-identical for every in-repo invocation (fm-spawn.shpinsFM_STATE_OVERRIDEto the task home's ownstatefor cloud spawns, and every test invocation passesFM_HOMEalone), and the change is what makes the money document singular across the split, but it is a semantic shift and not a pure re-anchor.bin/fm-spawn.shalso now checks the marker's CONTENT againstFM_SPAWN_PARENT_TASKat startup, so a typo refuses before any lifecycle lock is taken or any convergence artifact (including the copied provider credential) is staged.Round-2 verification
Zero-diff re-proved and widened to the release lane: ZERO-DIFF on 22 of 22 fenced functions,
CHANGED: none, now also coveringrelease_receipt,verify_release_against_worker,proof_template,command_releaseandcommand_surrender.fm-worker-lifecycle.test.shfm-spawn-cloud.test.shAdjacent suites re-run at this head, all rc=0:
fm-spawn-backlog7,fm-spawn-batch3,fm-spawn-dispatch-profile20,fm-spawn-provision63,fm-secondmate-cloud-monitor37,fm-secondmate-liveness16,fm-secondmate-harness25,fm-account-directory35,fm-bootstrap27,fm-auto-reap20.bin/fm-lint.shrc=0 over 246 scripts;bin/fm-lint-node.shrc=0.New mutation proofs
env["home"]again (dropauthority_home)WORKER AUTHORITY REFUSED: ordinary task metadata authority is absent- the admitted child is stranded again, which is precisely the findingtask_homeKeyError: 'task_home'- the path must survive onto the record the release lane actually seesauthority_homedrops the recorded-home_bindingfencea foreign task home minted receipts: authoritative endpoint/report/landing/account/worktree receipts writtenshapes the canonical reader refuses were ADMITTED: ['relative home', 'path through a symlinked component', 'dot-dot component', 'one corrupt trailing line']queued mark-1 generation gen-m1 for one isolated author worker- the PRIMARY's own home is accepted as a task homequeued mark-3 generation gen-m3 for one isolated author workerthe bindings are minted from the task home before it is authorizedHonest note on the registry mutation: the permissive reader admitted four of the six shapes rather than all six. The remaining two (
trailing spaces,one home under two ids) refused under the mutation because the four preceding admits had already consumed the default fan-out cap of 4, so they refused as over-cap children rather than on registry grounds. The four admits already prove the finding; at HEAD all six refuse on registry grounds, and the unit asserts that explicitly.Remaining honest limits, restated
data/secondmates.mdcan authorize a directory. That was always true of that document; this change makes the money path read it exactly as strictly as every other consumer does.fm-secondmate-cloud-monitor.sh/.pypassingFM_SPAWN_TASK_HOME) is still NOT in this PR; a sibling change owns those files. With finding 1 fixed, a child created once that lands is releasable through the ordinary lane rather than stranded.CONFIGstill stays on$FM_HOME; unchanged from round 1 and still a deliberate scope boundary.--task-homeis ever accepted without the complete parent pair, or the marker, home-shape or registry link is dropped, orauthority_home's fence is removed, any directory on the box could mint requests against the primary's money document, or redirect the release authority at a foreign home. Every one of those is separately mutation-proven above.