Skip to content

feat(worker): secondmate release authority and compartment status (R2/R3 PR 6) - #271

Merged
ruby-dlee merged 21 commits into
mainfrom
fm/secondmate-release-authority
Aug 20, 2026
Merged

feat(worker): secondmate release authority and compartment status (R2/R3 PR 6)#271
ruby-dlee merged 21 commits into
mainfrom
fm/secondmate-release-authority

Conversation

@ruby-dlee

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

Copy link
Copy Markdown
Owner

Closes the R2/R3 release lane for secondmate compartments: the authority tool gains a compartment evidence mode, surrender gains --confirm-orphan-children (retiring AMENDMENT 1's temporary live-children bypass), bounded status projects live compartments, and docs/azure-workers.md carries the B.9 delta sentences.

Design refs

R2R3-DESIGN.md B.7 (release and the receipts), B.9 (doc rewrites), C item 6 (this PR's scope), B.8 crash matrix row 3, AMENDMENT 1 (the bypass this closes).

What changed

Compartment evidence mode (bin/fm-worker-authority.py). Same fm.worker-release/v2 bundle, same five receipt names, selected by the controller-owned worker role:

  • endpoint: monitor pane absent, through the same fm_backend_target_state oracle (unchanged).
  • report: session closeout - the monitor's terminal status file, the chained close ack in its durable state, and report_evidence's ordered completion.md contract reused verbatim.
  • landing: every chained outbox bundle landed into the local home worktree (or provably none), AND the home head descending from the assignment's exact starting repository generation.
  • account: unchanged.
  • worktree: home quiesced - exact repo root, no uncommitted or untracked work, no unlanded bundles; advisory for children, whose refusal command_release owns.

verify_release_against_worker and release_receipt are untouched; the compartment bundle verifies through them unmodified (zero diff, asserted below).

Children-quiesced in command_release: verified already landed in PR 1 at the exact refusal string. No code change; this PR adds the positive control.

Surrender orphan confirm: a parent whose queue holds non-complete children refuses unless --confirm-orphan-children. With the flag, every live child's queue entry gains a durable reparented_to: "primary" note in the same lock hold that records the surrender.

Status projection: a compartments section built from controller.json fields only, additive.

Adversarial review round (all findings addressed)

An adversarial review found two merge blockers, both proven end to end. Both are fixed with units and mutation proofs.

1. BLOCKER - the evidence mode trusted a local, operator-writable metadata line. Mode selection read kind= from the task meta and never consulted the controller-owned role, though the whole worker record was already in hand. Proven: flipping one meta line (plus planting two compartment state files) let a role=author worker holding an unlanded commit mint all five receipts verdict=proved, pass the real release byte-path, and move the queue to releasing with the work still only local. Fixed: role decides, and role/kind disagreement refuses in both directions, fail-closed, before any evidence runs. Second half, same finding: the compartment landing receipt was bound only to local files, with no tie to controller-owned repository lineage; it now applies the same merge-base --is-ancestor tether against bindings.repository_generation that the ordinary lane uses.

2. BLOCKER - the landing proof was anchored inside the attacker's write set (three rounds). Each round narrowed it without fixing the shape:

  1. Enumeration only: an emptied mailbox read as "provably none".
  2. A cardinality check against the durable monitor state - but that file sits in the same state/ directory as the mailbox, so F1/F2/F3 still minted.
  3. Content verification of the chain - genuinely strong, and it killed F1/F2/F3 - but the chain is anchored at one end by a public genesis constant and at the other by verified_tip read from that same attacker-writable file. So G1 (fresh self-consistent chain), G2 (one-entry re-genesis), and T7 (M9's forgery plus exactly one more field write, entry 1 left byte-identical) all minted and passed the real release CLI.

The tip is the security boundary, so it now comes from the only trustworthy place in the system - the same controller-owned provenance that closed blocker 1:

  • New compartment-chain-tip command records the verified tip on the worker record under the controller lock, monotonically: a rewind refuses, and the same sequence with a different digest refuses. Deliberately not part of the message lane - PR 3's invariant is that message ops touch no lifecycle state and a static test pins it, so recording the tip there would trade one hole for another.
  • The authority reads the tip only from the worker record, and absent refuses rather than falling back (Option B's safe default, kept alongside Option A's mechanism). A static test now asserts state.get("verified_tip") appears nowhere in the authority.

Consequence, stated plainly: PR 4's monitor must be taught to call compartment-chain-tip when its tip advances. Until it does, no compartment can release through the ordinary authority - they exit through surrender, which is the sanctioned lane and already exists. The refusal says exactly this.

Verified on the reviewer's own harnesses repointed at this head: r3.sh - F1/F2/F3/G1/G2/G3/G4 and H1-H9 all refuse; t7e2e.sh, extended to record a controller tip so it can distinguish fixed from bricked - the honest compartment with its bundle landed MINTS (rc=0), the honest-unlanded one refuses, M9's forgery refuses at the content address, and T7 refuses at does not reproduce the controller-owned verified tip.

2b. BLOCKER (round four) - landed_bundles was a bare assertion. With an honest content-verified chain and an honest controller-owned tip, the receipt still took LANDING on trust: landed came straight out of landed_bundles in the attacker-writable durable file, and the home worktree was never checked for the bundle's commits. Naming a digest there minted all five receipts and passed the real release with the commits nowhere in the home - one file write, no CLI and no hashing, falsifying the receipt's own sentence.

Fixed by proving it from material already on disk: each declared or collected bundle's own tip commit (git bundle list-heads) must be an ancestor of the home worktree's HEAD. landed_bundles is now advisory and never decides; a declared bundle whose collected file is gone refuses, because nothing then names the commits it carried. Both the landing and worktree receipts share one proof function.

Verified on the reviewer's r4b.sh, extended with a real git bundle (theirs used a placeholder byte string, so it died at bundle parsing rather than at ancestry) and an honest control:

  • L0 landed_bundles=[] (truth) → refuses: its commit 100e7253247e is not reachable from the home worktree head
  • L1 landed_bundles=[65268244...] (the lie) → refuses identically
  • L2 work genuinely landed, landed_bundles=[]rc=0 MINTED

Accepted trust boundary, stated plainly: compartment-chain-tip attests without verifying. It never reads the mailbox, so a CLI-capable caller can record a tip for a chain they are about to plant (the reviewer's A1 and A2). This is not merge-blocking - round four raised the bar from "write a file under state/" to "invoke the controller CLI", the same class as role - but the verb is an unverified attestation whose whole value is caller trust, and no privilege separation exists between "can write state/" and "can exec bin/fm-worker-lifecycle.sh". Making the verb verify the chain would only relocate the trust, so it is documented rather than papered over.

3. MINOR - dangling .chain-break symlink bypassed the freeze. exists() follows symlinks; now os.path.lexists, matching the is_symlink() care taken elsewhere in the file.

4. MINOR - the compartment worktree receipt was laxer than the ordinary one. It used --untracked-files=no, so untracked never-added work passed. The justification did not survive scrutiny: the repo's .gitignore already excludes the home's runtime paths, which --untracked-files=all never lists anyway, and the monitor's gate answers "can I fast-forward here" while a release receipt answers "is anything left". Now --untracked-files=all, wrong justification deleted.

5. MINOR - orphaned_children was unconditional. Every receipt's evidence_digest is taken over the surrender block, so an unconditional "orphaned_children": 0 moved all five digests on the ordinary childless lane. Now scoped to surrenders that actually orphaned children, pinned by a unit that recomputes each digest. My earlier claim that the childless matrix was "byte-unchanged" was true of the refusals but not of the minted bytes; it is now true of both.

6. Residuals, confirmed and recorded.

  • report_evidence is text.find() plus monotonic position, not a heading contract, so it accepts headings inside a fenced code block, mid-sentence in prose, concatenated with no whitespace, or as a body-less skeleton. Per B.7 it stays verbatim (it serves the ordinary lane too), so secondmate_report_sections adds a compartment-only layer requiring each heading to open its own line, outside any fence, with content. R8 - a single . as each section body - still passes that layer; it is cosmetic, cannot hide unlanded work or a missing closeout, and closing it would mean inventing a prose-quality bar the ordinary lane does not have.
  • The content address binds the parsed object, not the file bytes (the body is re-canonicalized after json.loads), so trailing newlines, pretty-printing, and raw key reordering are tolerated. Any semantic change moves the digest, so it buys an attacker nothing - now stated in the docstring rather than left for the next reader.
  • LOW, parity not regression: a home that commits its own .gitignore with secret* and leaves secret_work.py on disk proves as quiesced, because --untracked-files=all does not list ignored files. This is byte-for-byte the ordinary lane's check.

Named follow-up, NOT fixed here: worktree redirection (pre-existing, both lanes)

bin/fm-worker-authority.py takes the compartment home path from values["worktree"] in the attacker-writable task meta, and worktree_binding is only copied into the proof, never checked against that path. Cloning the home into a throwaway directory, fetching the bundle there, and pointing worktree= at the clone MINTS and releases while the real home is still at base.

This is pre-existing on base main and affects the ORDINARY lane too (proven by the reviewer against 38ae18ef), so this PR inherits it rather than introducing it, and it is deliberately out of scope here.

The cheap fix for the follow-up: worktree_binding is digest_value({"worktree": str(worktree), "git_dir": str(git_dir)}) (fm-worker-lifecycle.py), and secondmate_worktree_evidence already computes both values - recomputing that digest and comparing it against worker["bindings"]["worktree_binding"] refuses the decoy and passes every honest control. Applying the same to worktree_evidence closes the ordinary lane.

Test evidence

tests/fm-worker-lifecycle.test.sh - 30 units (25 before, 5 added):

ok - surrender refuses live children without the orphan confirmation and durably reparents them with it
ok - a childless surrender mints its original receipt bytes with no orphan key
ok - a finished child unlocks the exact parent release the live child refused
ok - bounded status projects live compartments with children counts, TTL anchor, and optional legs
# fm-worker-lifecycle.test.sh: all assertions passed

tests/fm-worker-authority-secondmate.test.sh - new, 5 units, hermetic:

ok - every secondmate evidence leg refuses distinctly and the ack mapping binds all three terminals
ok - the secondmate bundle proves five receipts and releases through the unmodified verify path
ok - evidence mode selection follows the exact task metadata kind
ok - the controller-owned worker role decides the evidence mode in both directions
# fm-worker-authority-secondmate.test.sh: all assertions passed

The landing units cover all three forgeries verbatim (F1, F2 plain and F2 isolating, F3 across five malformed shapes plus six malformed tips), a re-genesis chain, truncated/wiped/symlinked mailboxes, the lineage tether, and the honest "provably none" shape. The role cross-check unit reproduces the reviewer's escalation exactly: a real bare origin with the base commit pushed and an unlanded commit on top, so the ordinary lane reaches its own landing proof and refuses. It asserts both refusal directions, the ordinary control, and a positive control. The mailbox units cover emptied-in-place, wiped, symlinked, verified-tip-only, and genuinely-empty-and-provable. The e2e unit still drives the real authority tool and the real release CLI against real durable controller state.

Fixing finding 2 exposed that my original fixture was unrealistic (delivered_sequence=2 with a single message file); it now builds a properly chained 1..N mailbox, which is a truer pin on PR 4's contract.

Mutation proofs (13 total; each red then reverted, tree verified clean)

# Mutation Red output
1 Drop surrender children check not ok - the surrender orphan-confirm contract is not enforced
2 Drop reparented_to stamping KeyError: 'reparented_to'
3 Missing terminal status still proves no refusal: lacks the terminal session status
4 Empty compartments projection AssertionError: []
5 Role cross-check trusts meta kind again AssertionError: (0, '') - rc 0 where 2 required
6 Drop mailbox completeness check no refusal: a rewound or truncated outbox
7 .chain-break follows symlinks again no refusal: frozen by a recorded outbox chain break
8 Compartment worktree ignores untracked no refusal: not quiesced: uncommitted or untracked work remains
9 Stop verifying the content address no refusal: content differs from its content address
10 Coerce durable state instead of refusing carries no verified chain tip where the delivered-sequence refusal was required
11 Anchor the tip in monitor-local state again no refusal: controller-owned worker record carries no verified chain tip
12 Drop the landing ancestry check AssertionError: L0: unlanded work proved landed
13 Make landed_bundles authoritative again AssertionError: L1: a lying landed_bundles still proved landing

Isolating 9 and 11 each took several attempts, because cheaper gates kept catching my first synthetic forgeries and would have let me claim proofs that isolated nothing. The isolating attack for 9 keeps the mailbox exactly as the monitor wrote it and rewrites only the leg summary's body (deleting the collected bundle file too), so only the SHA-256 recompute stands. For 11 the tip must be genuinely absent from the worker record, not a sentinel value.

PR-4 coupling

Re-verified after rebase against MERGED bec75a92 (PR #269), byte-identical to the branch tip originally read: terminal status values and their leg-summary acks, last_summary / landed_bundles / kept_bundles / delivered_sequence / verified_tip, and the mailbox naming plus sticky .chain-break. Nothing is owed back to PR 4; its own suite stays green.

Verification

  • bin/fm-lint.sh (full repo, 246 scripts): rc 0.
  • Registrations verified in test-capabilities.tsv and behavior-test-durations.tsv.
  • Reviewer harness (rv/atk/e2e.sh, repointed at this head): F1/F2/F3 refuse, honest control mints, honest-unlanded refuses.
  • verify_release_against_worker: zero diff against origin/main.
  • No live Azure exercise; billable acceptance remains design C item 7.

@ruby-dlee ruby-dlee changed the title feat(worker): secondmate release authority, orphan confirm, and compartment status (R2/R3 PR 6) feat(worker): secondmate release authority and compartment status (R2/R3 PR 6) Aug 20, 2026
@ruby-dlee
ruby-dlee force-pushed the fm/secondmate-release-authority branch from 8aae62c to c6f3a12 Compare August 20, 2026 14:12
@ruby-dlee
ruby-dlee force-pushed the fm/secondmate-release-authority branch from 1cb4481 to a0d8ad0 Compare August 20, 2026 16:15
@ruby-dlee
ruby-dlee merged commit 6881c73 into main Aug 20, 2026
13 checks passed
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