Skip to content

feat(secondmate): hand the compartment's own home to the child spawn as the task home - #279

Merged
ruby-dlee merged 1 commit into
mainfrom
fm/compartment-monitor-task-home-v2
Aug 21, 2026
Merged

feat(secondmate): hand the compartment's own home to the child spawn as the task home#279
ruby-dlee merged 1 commit into
mainfrom
fm/compartment-monitor-task-home-v2

Conversation

@ruby-dlee

@ruby-dlee ruby-dlee commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Third and last piece of the R2/R3 compartment-child lane. #278 (merged as e2683acc) built the authorized task home in bin/fm-worker-lifecycle.py and taught bin/fm-spawn.sh to accept FM_SPAWN_TASK_HOME, derive owner_kind from the task home's marker, and forward --task-home. Nothing set that variable. This does, from the compartment monitor, and the lane admits end to end.

Base is main. This branch touches ONLY bin/fm-secondmate-cloud-monitor.py and its test file; it carries no #278 content.

What changed

spawn_environment now sets FM_SPAWN_TASK_HOME to the compartment's own home. FM_HOME still stays exactly as the monitor received it, because FM_HOME is what names the ONE money document, and moving it is the trap the docstring refutes.

The home comes from --home, which the wrapper reads from the durable <primary state>/<id>.cloud-worktree the compartment's own spawn wrote (bin/fm-secondmate-cloud-monitor.sh:75,395). It is not reconstructed from a task id, a naming convention, or a registry read. self.spawn_home is the CONTROLLER's home and is not it.

The assignment is made after the method's FM_STATE_OVERRIDE and FM_SECONDMATE_* pops, and its name sits outside both popped namespaces. The FM_STATE_OVERRIDE pop is an interlock, not a nicety: fm-spawn refuses FM_SPAWN_TASK_HOME combined with FM_STATE_OVERRIDE, so a leg-scoped state override reaching this argv would refuse the whole lane. Correcting an earlier revision of this body and of the docstring: the code pops FM_STATE_OVERRIDE only. FM_DATA_OVERRIDE and FM_PROJECTS_OVERRIDE are NOT popped. fm-spawn refuses those two alongside a task home as well, so an inherited one fails the lane closed rather than silently re-pointing the child's data/ or projects/; it is a refusal this method relies on, not one it performs. The words were broadened, not the code.

Four call sites move with the split, because fm-spawn now reads them from the task home rather than from FM_HOME: the child's project resolution, its backlog row, its brief, and its recorded execution result. Without these the lane still refuses, just later and in fm-spawn's own voice (see MUT-C below, which shows the brief being looked for under the primary).

FM_SPAWN_TASK_HOME is deliberately NOT in SPAWN_CLOUD_ENV_ALLOWLIST and does not reach <id>.cloud-env. That file is re-sourced by every LATER execute and release for the id, so a name landing there pairs a foreign home with all of them rather than with this one request. The durable record of the split is the queue item's own task_home field, which the release lane reads back through authority_home.

The docstring is rewritten. It used to explain at length that the method deliberately did nothing because the capability was missing; that is now false. It states what the method does, and KEEPS both rejected-alternative paragraphs (the FM_AZURE_WORKER_STATE_DIR durable trap, and moving FM_HOME at a second money document), because that reasoning is what stops a future edit reintroducing either. The module docstring's "NOT YET POSSIBLE" paragraph is replaced for the same reason.

One simplification: resolve_project is the single place that decides the compartment's own home is usable, and it refuses by name before any side effect. An earlier revision of this branch repeated that check in spawn_child; that duplicate is gone, so the invariant has one owner.

Proof 1: the lane end to end, read back from the money document

test_child_request_through_the_real_fm_spawn_with_crew_dispatch used to PIN the refusal, with a comment saying to rewrite it to assert admission once an assertable owner kind and a task-home parameter landed. Both landed. It now asserts admission, through every real component: a real compartment spawned by the real fm-spawn.sh, a real runner-emitted child request, the real relay, the real fm-spawn.sh again, and the real fm-worker-lifecycle.py controller.

The child's actual entry in the PRIMARY's controller document, dumped from a run of that unit:

{
  "assignment_generation": "asg-00000002",
  "home_binding": "fdd464e9e8ebcc8f7ecf8a9acf8de615747badd2587a4241d684aa0dc7e666f0",
  "owner_kind": "secondmate",
  "parent_task": "mast",
  "parent_task_generation": "spawn:a00f7262dd54d5d2",
  "role": "author",
  "schema": "fm.worker-request/v1",
  "slot": 2,
  "status": "assigned",
  "task": "mast-c9e2a9a87",
  "task_generation": "spawn:a0b18a95d1672ba8",
  "task_home": ".../spawn-real-spawn/mast-home"
}
document home_binding: 7f7aecaf91447a666f0077baf59c9928b7bbf09103d6ef648e054d745c5b3691
primary home:          .../spawn-real-spawn/main-home
compartment home:      .../spawn-real-spawn/mast-home
parent compartment worker record: "children_total": 1, "role": "secondmate"

owner_kind=secondmate, the parent pair present, home_binding = sha256 of the COMPARTMENT's home while the document's own home_binding = sha256 of the PRIMARY's, children_total incremented on the parent's worker record, and no second controller document under the compartment home. The unit asserts every one of those, plus that the acceptance was delivered into the compartment inbox and a durable .accepted-* record exists with no .refused-*.

Nothing in this lane was simulated. The only fixture is the Azure provider, as in every suite here.

Proof 2: ordinary spawns are byte-identical

git diff --name-only origin/main returns exactly two files, neither of which is on any ordinary spawn path:

bin/fm-secondmate-cloud-monitor.py
tests/fm-secondmate-cloud-monitor.test.sh

bin/fm-spawn.sh and bin/fm-worker-lifecycle.py are untouched by this branch, so an ordinary crewmate spawn and a LOCAL secondmate home requesting its own crewmates build the identical request argv they build on main. spawn_environment is reachable only from the compartment child relay. The suites covering those lanes pass unchanged (below), including test_spawn_gate_off_flag_is_byte_identical and test_spawn_forwards_the_parent_pair_into_the_request.

Proof 3: mutations

MUT-A, the allowlist exclusion. ADMIT-red. Added FM_SPAWN_TASK_HOME to SPAWN_CLOUD_ENV_ALLOWLIST in bin/fm-spawn.sh and ran the end-to-end unit. The generated <compartment home>/state/<child>.cloud-env, read off disk:

export FM_SPAWN_CLOUD_WALL_SECONDS=3600
export FM_WORKER_PROVIDER_COMMAND=python3\ .../provider.py
export FM_SPAWN_TASK_HOME=.../spawn-real-spawn/mast-home
export FM_AZURE_DEPLOYMENT_GENERATION=dep-one
...
grep -c FM_SPAWN_TASK_HOME -> 1
not ok - FM_SPAWN_TASK_HOME was persisted into the child's durable cloud environment, where it becomes a durable trap

Unmutated, the same file and the same grep:

export FM_SPAWN_CLOUD_WALL_SECONDS=3600
export FM_WORKER_PROVIDER_COMMAND=python3\ .../provider.py
export FM_AZURE_DEPLOYMENT_GENERATION=dep-one
export FM_AZURE_NAMING_PREFIX=fmtest
export FM_AZURE_OWNER_TAG=owner
export FM_AZURE_SUBSCRIPTION_ID=11111111-1111-4111-8111-111111111111
export FM_AZURE_WORKER_IDLE_COOLDOWN_SECONDS=0
grep -c FM_SPAWN_TASK_HOME -> 0 (absent)

The red is the durable pin actually landing in the file, not a message change. This is the grep of an actually-generated .cloud-env, not an assertion about one.

MUT-B, the rejected alternative. Replaced env["FM_SPAWN_TASK_HOME"] = str(self.home) with env["FM_HOME"] = str(self.home), the move the docstring refutes. Red:

ELASTIC WORKER REFUSED: child request parent mast@spawn:a52c57d92c593331
is not an assigned secondmate compartment

Exactly the outcome AMENDMENT 2 predicted: it refuses at parent liveness, naming the wrong cause. Probed the compartment home directly in the same run: no second controller.json was created, so on merged code that alternative refuses before it can fork the money authority. Stated as measured, not as a guarantee for every world.

MUT-C, the assignment deleted (the pre-change state). Red:

error: no brief at .../spawn-real-spawn/main-home/data/mast-c39299c3e/brief.md

The refusal names the PRIMARY's home, which is where the whole lane pointed before this change.

MUT-D, the task home sourced from self.spawn_home instead of self.home - the home confusion this split exists to prevent. Red:

error: FM_SPAWN_TASK_HOME is not a seeded secondmate home: .../spawn-real-spawn/main-home

What I could not prove, stated plainly. MUT-B, MUT-C and MUT-D are REFUSAL-reds, not admit-reds, and I am not going to dress them up as admits. I could not construct an admit-red for the env assignment itself, and the reason is structural rather than a gap in the testing: every shape this change can produce is caught downstream by merged code that fails closed. A missing or wrong task home yields owner_kind=primary, which verify_request refuses the moment it carries a parent pair; a task home pointed anywhere that is not a seeded secondmate home is refused by fm-spawn's marker check; and a task home that survives that is still authorized inside the controller lock against the marker, the primary's registry, and the parent's assigned entry. The one place this change is the sole thing between the system and a wrong outcome is the durable .cloud-env pin, and that is MUT-A.

Related: I originally also carried a tighten-only owner_kind guard in fm-spawn.sh (refusing a real secondmate home from naming a marker-less task home to mint primary). It was discarded before this PR, because #278's requirement that the task home BE a seeded secondmate home already makes that direction unreachable - MUT-D is the executed evidence of that refusal.

Rebase onto #277 (2f359f97)

Rebased. The rebase produced exactly two conflicts, both in bin/fm-secondmate-cloud-monitor.py and both PURE PROSE: the module-header bullet and the spawn_environment docstring. env["FM_SPAWN_TASK_HOME"] = str(self.home) sits in a separate hunk #277 does not touch and auto-merged one line below the closing marker, so taking #277's newer-looking text on either block would have shipped a file whose docstrings state at length that the relay "does not set FM_SPAWN_TASK_HOME" and "sets no FM_SPAWN_TASK_HOME", directly above the line that sets it. No test asserts on docstrings, so nothing would have caught it.

Both blocks were resolved in favour of the post-#279 semantics, keeping #277's genuinely new information rather than discarding it: the summary of what #278 landed, that authorize_task_home proves the chain under the same lock hold that inserts and nothing in it is self-authorizing (marker, canonical registry link, then the unchanged child bounds), the historical warning that an earlier revision wrongly blamed verify_state's home fence, and #277's "move the TASK home, never FM_HOME" punchline. The module bullet that read "STILL REFUSED HERE" now reads "ADMITTED HERE, and bounded", and names what still bounds it: depth one, fan-out, lifetime total and parent liveness, enforced by enforce_child_bounds under the one lock over the one document FM_HOME still names.

#277's note calling "wiring the compartment's own home through as the task home" a deliberate follow-up appeared twice, both times inside the two conflict blocks, and both are gone. This PR is that follow-up. git show 2f359f97 confirms no third instance in the .py, and neither bin/fm-secondmate-cloud-monitor.sh nor docs/azure-workers.md carries a stale task-home claim.

Both paragraphs that exist only on my side survived, confirmed by grep on the rebased file:

$ grep -n "ORDERING HAZARD" bin/fm-secondmate-cloud-monitor.py
1343:        ORDERING HAZARD, deliberately survived: this method pops
1383:        # After the pops, on purpose: see ORDERING HAZARD above.
$ grep -n "deliberately NOT on that allowlist" bin/fm-secondmate-cloud-monitor.py
1362:            FM_SPAWN_TASK_HOME is deliberately NOT on that allowlist for the

tests/fm-secondmate-cloud-monitor.test.sh auto-merged. Both PRs' units survive and my fixture changes are intact. On the one reconciliation that matters: no #277 unit pins the refusal my unit now asserts as an admission. test_spawn_forwards_the_parent_pair_into_the_request does assert parent_task is owned by secondmate-owned author requests only, but on a different shape - a --secondmate COMPARTMENT spawn (role=secondmate) carrying a parent pair, which must refuse by the depth-one bound. Mine is a role=author child, which must be admitted. The other refusal units are schema-validation refusals that never reach command_request. No two units assert opposite outcomes on the same lane.

MUT-A was re-run against the rebased head and still reproduces as an admit-red.

Suites

All through python3 tests/run-one.py, never bare bash, on the rebased head 1ce8c12f, with exit codes captured:

suite exit ok - not ok
tests/fm-secondmate-cloud-monitor.test.sh 0 50 0
tests/fm-spawn-cloud.test.sh 0 20 0
tests/fm-worker-lifecycle.test.sh 0 36 0

Correcting an earlier revision of this body, which said the monitor suite had "39 units": that number was never measured. The rebased head has 50 dispatch entries emitting 50 ok lines.

Disagreement I have to flag rather than paper over. I was told fm-spawn-cloud and fm-worker-lifecycle are red on macOS at base on merged main, host-coupled, green in CI, and not my regression, and asked to state that baseline instead of claiming a pass. I cannot state it, because I could not reproduce it. I ran both at base in a clean detached worktree on 2f359f97 and on this head, on macOS (Darwin 25.6.0), and got exit 0 with zero not ok every time:

base 2f359f97  fm-spawn-cloud.test.sh       EXIT=0  ok=20  not ok=0
base 2f359f97  fm-worker-lifecycle.test.sh  EXIT=0  ok=36  not ok=0
head 1ce8c12f  fm-spawn-cloud.test.sh       EXIT=0  ok=20  not ok=0
head 1ce8c12f  fm-worker-lifecycle.test.sh  EXIT=0  ok=36  not ok=0

I also re-ran fm-spawn-cloud with FM_HOME pointed at the operator home in case the operator environment was the trigger: still exit 0. So the reported red is real for whoever saw it but is environment-specific in a way this machine does not reproduce, and I am not going to assert a baseline I did not observe in either direction. What I can say is the measured one: green at base and green on this head, identically, so these suites are unchanged by this PR either way. If the reviewers' environment is the authority here, the delta between the two setups is worth finding before it is written into a doc as settled.

Test-fixture changes worth a reviewer's eye

The monitor suite's fixture world encoded the OLD rule in a comment: that the child's authorities live in the home the spawn runs under. Under the split they live in the task home, so make_world now seeds projects/, data/ and state/ in the compartment home, and the assertions follow them there. The two homes are given DIFFERENTLY NAMED projects (alpha in the compartment, primary-only in the primary) on purpose: a regression that read the primary's projects/ now names the wrong directory instead of accidentally naming the right one.

…as the task home

The compartment child lane had one gap left after the authorized task home
landed: nothing set it. spawn_environment deliberately did nothing clever
because the capability did not exist, so every child request was minted
against the controller's home, came out primary-owned, and refused at
verify_request naming owner_kind.

The monitor now sets FM_SPAWN_TASK_HOME to the compartment's own home, taken
from --home (the leased home the compartment's own spawn recorded in
<primary state>/<id>.cloud-worktree), never reconstructed. FM_HOME still
stays the controller's, because FM_HOME is what names the ONE money
document; the assignment is made after the FM_STATE_OVERRIDE and
FM_SECONDMATE_* pops, and its name is deliberately outside
SPAWN_CLOUD_ENV_ALLOWLIST so it is consumed once instead of persisted into
<id>.cloud-env as a durable foreign-home pin.

The split moves four call sites with it, because fm-spawn now reads them
from the task home: the child's project resolution, its backlog row, its
brief, and its recorded execution result. A relay pass that cannot locate
the compartment's own home refuses by name and delivers that refusal,
rather than silently falling back to the primary.

The docstring is rewritten to say what the method now does, keeping both
rejected alternatives (the FM_AZURE_WORKER_STATE_DIR durable trap, and
moving FM_HOME at a second money document) as the reasoning that stops a
future edit reintroducing either.

The real-spawn unit that pinned the old refusal now asserts admission, read
back from the money document: owner_kind=secondmate, the parent pair,
home_binding naming the compartment's home inside a document whose own
home_binding names the primary, children_total incremented, no second
controller document, and no FM_SPAWN_TASK_HOME in the generated cloud-env.
@ruby-dlee
ruby-dlee force-pushed the fm/compartment-monitor-task-home-v2 branch from 314284e to 1ce8c12 Compare August 20, 2026 23:53
@ruby-dlee
ruby-dlee merged commit 5a35dd6 into main Aug 21, 2026
13 checks passed
@ruby-dlee
ruby-dlee deleted the fm/compartment-monitor-task-home-v2 branch August 21, 2026 00:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant