feat(worker): record verified compartment chain tips from the monitor - #277
Merged
Conversation
…ip refusal markers
ruby-dlee
force-pushed
the
fm/monitor-records-chain-tip
branch
from
August 20, 2026 23:16
baeec1d to
028baf1
Compare
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.
The gap
PR #271 moved the compartment landing anchor to controller-owned state:
compartment-chain-tiprecords the verified outbox chain tip on the worker record under the controller lock, andfm-worker-authority.pyreads the tip ONLY from there, refusing when it is absent with "exits through surrender until its monitor records tips with compartment-chain-tip".Nothing called it. On main,
grep -c compartment-chain-tip bin/fm-secondmate-cloud-monitor.sh bin/fm-secondmate-cloud-monitor.pyreturns 0 and 0, so every secondmate compartment could only take the sanctioned-but-degradedsurrenderexit. This wires the missing call.What this builds
bin/fm-secondmate-cloud-monitor.py process-mailboxnow attests the tip whenever local verification ADVANCES it, and only then.verify_mailboxand after both stateful rewind checks, and before any delivery. The command attests without verifying (it never reads the mailbox), so the monitor must never report a tip it has not itself proved.(sequence, chain_digest)pair is durable in the monitor's own state (recorded_chain_tip), and an unchanged tip is skipped.message-put/message-collect, whose claim-exempt carve and PR feat(worker): claim-exempt secondmate message lane (R2/R3 PR 3) #266 static pin assert the message ops write no lifecycle state. A unit pins that nomessage-*invocation ever carries--chain-digest.FM_AZURE_*identity. No provider call is made: this command only takes the controller lock and writes one field. Timeout is a dedicated 300s rather than the 900s blob-transfer budget.Refusal semantics, and why
Monotonicity refusal (rewind, or same sequence with a different digest) FREEZES the lane through the same sticky
.chain-breakmarker, exit 3, delivering nothing. The record is monotone by construction, so this can only mean the controller's attested chain and the chain this monitor just proved are not the same chain. That is the same harm class the marker already exists for, and no later pass can heal it: the monitor's own stateful tip check catches this when its state file survives, and this is the case where the state file did NOT survive and the controller record is the only remaining memory."released work cannot record a compartment chain tip" is end-of-life, but it is never taken at face value.
command_compartment_chain_tipchecksrelease_proofBEFORE its monotonicity block (bin/fm-worker-lifecycle.py, therelease_proof is not Noneguard immediately preceding theverified_chain_tipcomparison; lines 4072 and 4074-4085 as of the rebase ontoe2683acc, deliberately named by symbol because feat(worker): authorized task home for compartment child requests #278 shifted them ~211 lines and this PR's own code comments strip line numbers for that reason), so a RELEASED worker answers a genuine rewind or fork with the benign string. Classifying on the string alone would let the one refusal class that must freeze arrive dressed as the one that must not, and the compartment would keep relaying a chain the controller's own record contradicts. The monitor therefore reads the held tip back out of the controller document and judges it: a contradiction FREEZES, an unreadable document falls to the retry class (closing on faith would silently downgrade the compartment tosurrenderfor good), and only a non-contradicting held tip closes the lane durably and quietly.That read-back is deliberately STRICTLY STRONGER than the controller's own rule, because it exists precisely to cover a rule the controller applies too late, and a check that merely mirrored the controller would inherit its blind spot. Monotonicity alone never contradicts a held tip strictly BELOW the proved sequence, whatever its digest: an attacker who mints a chain LONGER than the held tip but diverging BENEATH it gets no contradiction, so the forgery would close benignly, render, and land its bundle declarations.
chain_tip_forkstherefore also requires the proved chain to REPRODUCE the held tip's digest at the held tip's own sequence, which is the identical checksecondmate_verified_chaininbin/fm-worker-authority.pyalready performs before it will prove landing. A held sequence that cannot be looked up in the proved chain (not a positive integer, or past its end) cannot be reproduced and is never read as agreement.Repairing the ordering inside the controller (running monotonicity before the release gate, so a fork refuses as a fork whatever the worker's phase) is a follow-up owned by
bin/fm-worker-lifecycle.py, deliberately not touched here because PR feat(worker): authorized task home for compartment child requests #278 owns that file right now.Everything else warns and retries under exponential backoff (not assigned, wrong assignment generation, unreadable controller, failed invocation), recorded durably in
chain_tip_errorwithfatal: false,attempts, andnext_attempt_at. These are statements about who owns the worker right now, not about the chain, and are expected to be transient; freezing on them would wedge a healthy compartment. Because every attempt takes the controller lock, retries back off 30s doubling to a 1h cap keyed on the durable error, and an identical repeated refusal is recorded rather than reprinted. Without that, a 15s poll loop stuck on an ownership refusal would cost roughly 5,700 controller-lock acquisitions and 5,700 pane lines a day for one compartment. The backoff resets when the tip being reported changes, since that is a genuinely different call.The split is by the controller's own refusal texts, so a refusal this monitor does not recognise falls into the conservative warn-and-retry class rather than being mistaken for a fork.
Docs
docs/azure-workers.mdno longer implies surrender is the normal exit. It states that the monitor records tips so the ordinary release authority can prove landing, describes the advance-only recording and the durable skip, the not-the-message-lane boundary, all three refusal semantics including the released-branch readback and the named controller-side follow-up, and the backoff. It keeps the honest residue below.Test evidence
tests/fm-secondmate-cloud-monitor.test.sh: 37 units on main, 50 now (13 added). Registration audit (the harness registers by bare word, so a typo would silently exit 0): 50 registered, 50 unique, 50 defined, 0 registered-but-undefined, 0 defined-but-unregistered, 0 duplicates.Run as CI runs it (
python3 tests/run-one.py, which owns the process reaper; every backgrounded monitor is a session leader stopped by process-group kill):Sibling suites, same runner:
tests/fm-worker-authority-secondmate.test.shrc=0,tests/fm-worker-lifecycle.test.shrc=0.bin/fm-lint.shrc=0 over 246 scripts.How the units avoid mocking the callee:
bin/fm-secondmate-session.pyagainst the store. Three statements of the tip must agree: the monitor's durable state, the controller record, and the re-derivation.bin/fm-worker-lifecycle.pydeclares.bin/fm-worker-lifecycle.shagainst real controller documents built through the real module's ownenvironment()/empty_state(): the healthy record, the released-with-rewind freeze, the released-with-below-tip-fork freeze, and the released-with-genuinely-clean-tip close. Each released unit asserts the real CLI really did answer with the released string, so they pin the ordering hole itself rather than a fixture's imitation of it.Mutation proofs
All eight applied, proven red, reverted, and re-proven green. Mutations 1-4 were re-run against the post-review code, not just against the version they were written for.
record_chain_tipcall fromcommand_process_mailboxnot ok - the chain tip argv is not the exact contractnot ok - a tip was attested for a mailbox whose bodies were substitutednot ok - an unchanged tip was re-recorded (3 invocations for one tip)--lifecycle-bin "")not ok - ... the chain tip recorded through the monitornot ok - a released worker holding a contradicting tip did not freezenot ok - a backed-off pass still took the controller lock (2 invocations)not ok - the tip lane closed without reading the held tip backnot ok - a held tip below the proved sequence with a digest this chain does not reproduce was acceptedMutation 2 was deliberately re-done after a first attempt went red for the wrong reason (it changed the reported digest, so it failed the argv unit rather than the ordering one). The version above keeps the recorded values identical on a healthy chain, which is why the substitution assertion had to be added: it is the only case that separates "attested then verified" from "verified then attested".
Test-harness fix carried here
wait_forre-runs its argv on every retry, so any$(...)or<(...)in the arguments is evaluated ONCE at call time and the loop then re-tests a frozen value or an exhausted fd. Two instances are fixed, and both are replaced with in-shell predicates (inbox_has,worker_tip_recorded) rather thanbash -c, which would needexport -fto see the suite's functions:grep -q 'FIRSTMATE ACCEPTED' <(inbox_messages)was pre-existing and byte-identical on base, but this change inserts a subshell, a. "$CLOUD_ENV", and a lifecycle subprocess intoprocess_mailboximmediately beforechild_relayin the same loop iteration, so it sits in that racing window.test -n "$(recorded_worker_tip)"was introduced by this PR and had the same defect.A sweep of every
<(intests/found no other instance inside a retry loop (the remaining uses are single-shotcmp/comm/assert_no_greparguments), and everywait_forin this suite is now literal-argument or predicate.Honest limits
compartment-chain-tipremains an unverified ATTESTATION, as PR feat(worker): secondmate release authority and compartment status (R2/R3 PR 6) #271 documented: it never reads the mailbox, so its whole value is caller trust, and there is still no privilege separation between a caller who can write understate/and one who can executebin/fm-worker-lifecycle.sh. This change makes the honest monitor call it; it does not make the command self-verifying.chain_tip_closed, or arecorded_chain_tipequal to the tip about to be proved, makes the monitor skip the call silently and downgrades that compartment tosurrenderfor good. The failure direction is the safe one (no false landing proof), but the suppression path is real and is now stated in the doc.next_attempt_atis absolute wall-clock time and is persisted, so a backwards clock jump extends the wait by the size of the jump. Informational: the ladder is disclosed and the state file names the deadline.surrenderrather than the ordinary authority. Informational and operator-visible, not a correctness gap.surrender.Does a compartment release ordinarily now?
Yes, for the case this owns. After a monitor pass whose chain verified, the controller-owned worker record carries
verified_chain_tip, which is exactly whatsecondmate_chain_extentrequires and refuses without. Proven end to end against the REAL lifecycle CLI writing a REAL controller document, and the authority's own suite is green against that same shape.surrenderreturns to being the degraded exit rather than the only one.Rebased onto #278
Rebased onto
origin/mainate2683acc(PR #278, authorized task home for compartment child requests). The predicteddocs/azure-workers.mdconflict did not materialise: #278's edits are in the compartment-request section (~line 40) and this PR's are in the release section (~line 199), so the auto-merge is genuinely two disjoint regions rather than two stitched halves.mergeStateStatusis CLEAN.Two stale claims that #278 retired from the doc also lived in prose in this PR's own file, and the rebase is what made them false, so they are corrected here rather than left to rot:
bin/fm-secondmate-cloud-monitor.py's module docstring said a compartment child was "NOT YET POSSIBLE" pending "an assertable owner_kind on the spawn side and an authorized task-home parameter", the exact two capabilities feat(worker): authorized task home for compartment child requests #278 delivered. It now records that both landed, and that the lane still refuses here for a different and honest reason: this relay does not setFM_SPAWN_TASK_HOME, so the spawn's task home is the primary home,owner_kindderives as primary, andverify_requestrefuses the parent-paired request as before. Behavior is unchanged and the existing unit still pins that refusal.spawn_environment's docstring reasoned from the retired "FM_HOMEmoves for the spawn" premise. It now states feat(worker): authorized task home for compartment child requests #278's split (FM_HOMEkeeps the money-document job only and never moves;FM_SPAWN_TASK_HOMEcarries the requester's authority and identity), and notes that feat(worker): authorized task home for compartment child requests #278's answer is the same one this comment already reached: move the TASK home, neverFM_HOME. TheFM_AZURE_WORKER_STATE_DIRno-pin rationale is retained because the doc still says the same thing.Actually wiring the compartment's own home through as the task home would admit compartment children for real. That is a behavior change with its own bounds to prove, so it is named as a follow-up in the code rather than smuggled into the chain-tip lane. A grep for the retired claims (
blocking compartment children,Until an assertable,stays pinned to the local controller,NOT YET POSSIBLE,missing capability) acrossdocs/andbin/returns nothing.Deferred follow-up, now done
The released marker string is now added to the owner-pinned marker list in
tests/fm-worker-lifecycle.test.sh, alongside the second fork marker which was also unpinned there. Editingcommand_compartment_chain_tipand running only its owning suite now gives a local signal instead of relying on the cross-suite CI run. Proven load-bearing: typing the string wrong fails that suite withAssertionError: released work cannot record a compartment chain tipX/not ok - elastic worker static contract failed, rc=1.One follow-up remains: the controller-side ordering repair described under refusal class 2.