Repository navigation
Volume approval, its verifier and the Machine's operation records (E2) - #4051
Merged
Merged
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
…ords (E2) A Device signs one VolumeApproval for a Move, Mirror, Mirror removal, Restore, resize or seed, under a new volume purpose that no session grant can sign. VolumeWorkflow::permits is the transition graph each Machine checks a step against. Each Machine keeps its Volume operations in the approvals log: open after a first step verifies inside its window, ended for good after, with recipient grants and withdrawals. A new owner of a copy ends the old one, and a Move's source with no decision records Resumed. Nothing calls these yet.
…and keep a replacing Mirror through its own withdrawal A Move now owns the copy of each Mirror it keeps following, so admitting a later owner there ends the Move on that Machine. withdraw_volume takes the replacing Mirror operation whose Forget writes the withdrawal. That operation stays open and the withdrawal does not outrank its grants. It ends every other Mirror operation as before.
withdraw_volume no longer takes an operation to spare. Admitting the opening MirrorToForget of a Mirror approval that replaces this Machine's copy writes the withdrawal of this Volume and Machine in the same synced write, just before the replacement opens. The replacement then ranks after its own withdrawal and needs no exemption. Nothing else can withdraw that way: a step's verifier refuses an ordinary Mirror's Forget, a Move's, and a replacement naming another Volume or Machine, and a resent Forget on the open record withdraws nothing.
nick-potts
force-pushed
the
t3/sec04-e2-volume-approval
branch
from
November 10, 2026 12:59
184960e to
08ac63c
Compare
nick-potts
marked this pull request as ready for review
November 10, 2026 13:04
nick-potts
enabled auto-merge
November 10, 2026 13:04
github-merge-queue
Bot
removed this pull request from the merge queue due to invalid changes in the merge commit
Nov 10, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to invalid changes in the merge commit
Nov 10, 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.
Why
A compromised Cloud must not run code of its choosing on a Server or take data off it through a Volume Run. E1 refused Cloud's Container starts and seeds outright. E3 to E7 replace that refusal with one approval of the reviewed workflow, which each Machine checks for itself. This PR is the verifier they call: the signed statement, the graph of steps it permits, and the Machine's own record of what it admitted. No route calls any of it yet, so behaviour is unchanged.
Built on D4 (#4047), now merged. The records sit beside D4's rule records in the consumption log, and this branch is rebased onto
main.Summary
flowchart LR D[Device] -->|signs, purpose volume| S["Signed<VolumeApproval>"] S --> V["volume::verify<br/>signer is a Device · Cluster · Volume<br/>Machine named · permits(step) · window"] V --> A[admit_volume_step] A --> R[("approvals log<br/>operations · grants · withdrawals")] R -->|open: no window check| A R -->|ended: Replayed, for good| X[refused]Admission, as
e-plan.mdorders it:volumepurpose that only a Device signs, the same rule asPurpose::Rule. A browser key under a grant of every family is refusedMismatch { the signer }. A statement riding a proposal is refused. A passkey through its own prompt is not accepted yet: that needs thread B's passkey-direct helper (gap 6, E8).verify-volume-marksdoes not apply. A withdrawal must outlive the slot it removes. Operation records and withdrawals are never pruned. A clockless sequence ranks withdrawals against admissions. A grant under an operation admitted before a withdrawal of that recipient is refused, which keeps a removed Mirror from being re-granted by a Move that was already running.MirrorToForgetof a Mirror approval withreplacethat names this Machine writeswithdrawn(V, this Machine)in the same synced write, just before the replacement opens (e-plan E6). That ends every other Mirror operation here, and the replacement ranks after its own withdrawal. A resentForgeton the open record withdraws nothing.withdraw_volume(V, M)takes no operation to spare, so a caller cannot exempt one.Snapshotsscope is accepted. Standing grants go to Mirrors only.VolumeStep::Startis permitted only on a Move's two Machines. A Restore never permits one.Left for later slices, against the plan's E2 checklist
approvalrequest fields andployz.volume.approval.v1. No route reads an approval yet. A capability advertised now would say "checks approvals" on a daemon that ignores them. E3 adds the fields, the capability and the first reader together. The requests do not deny unknown fields, so an older daemon already ignores the field.ApprovedVolumeChange. Its first reader is E3's CLI output, which runs throughui-copyagainst real output.permitschecks the transition only. E4 to E6 add the snapshot and receipt checks with their routes.DecisionholdsResumedonly. E5 addsHanded.ployz-verifyis untouched. E3 adds the firstBrokenflag with its first guard.Bets
volumeskey, written only when a record exists, and nothing writes one until E3. No catalog row and no request field yet.Purposegainsvolume, and theReplayedtext no longer says "restart", because Volume steps share it.Residuals
Named once, per the plan's "What remains", and left alone:
Security-bar finding from my own review: none open. One tightening went in. A Move's target could have taken a Standing grant, which serves every snapshot of the Volume.
grantnow accepts only aSnapshotsscope for it.Evidence
Rung 2 for each test below (crate unit,
cargo test). Each one fails if the behaviour named in its title breaks.ployz-core(cargo test -p ployz-core --lib authority::volume, 8 passed; all ofauthority, 119 passed):each_workflow_signs_its_golden_canonical_bodyployz-authority/v1\nvolume\na_device_approval_admits_each_step_its_graph_names_on_each_machinea Volume step).the_other_workflows_permit_only_their_own_stepseach_refusal_names_what_the_approval_lacksthe_window_bounds_the_first_step_with_clock_skewExpiredpastexpires_atplus skew, a future issue time, a lifetime over 15 minutesa_browser_key_under_a_grant_of_every_family_cannot_sign_a_volume_approvalthe signera_signed_approval_riding_a_proposal_is_refuseda_field_this_machine_cannot_read_is_malformedployzd(cargo test -p ployzd --lib volume_approval, 15 passed). Each record-level call opens the log afresh from disk, so every assertion reads what was synced:an_open_operation_finishes_after_its_window_and_an_ended_one_never_runs_againExpired, and after a clock rollback with no check at all. Once ended it isReplayed, its grant is not live, and it cannot be re-granted.an_open_operation_still_admits_only_its_own_body_volume_and_grapha_bounded_grant_serves_only_its_snapshots_and_ends_with_its_movea_withdrawal_ends_only_what_mirrored_that_volume_to_that_machinewithdraw(V, M)ends that Mirror and its grant. It leaves another Mirror and another Volume alone. A new approval never admitted before is admitted and granted.a_mirror_that_withdraws_itself_ends_its_own_pulls_and_a_followed_movea_withdrawal_outranks_a_grant_under_a_move_admitted_before_ita_new_owner_of_the_copy_ends_the_old_one_and_a_source_without_a_decision_resumesResumed, and the Move is thenReplayed. A Restore ends the Mirror. A Resize owns nothing and survives. On B the Move ends with no decision.a_later_owner_of_a_following_mirrors_copy_ends_the_move_thereReplayed. Failed before the fix.a_replacing_mirrors_opening_forget_withdraws_every_other_mirror_here_and_keeps_itselfForgetalone writes one withdrawal. The old Mirror isReplayed, the replacement'sNoneToMirrorpull passes after its window, and a Mirror of another Volume goes on. Failed with the derived withdrawal switched off.a_replacement_withdraws_once_and_a_later_withdrawal_ends_itForgetadds no withdrawal. A later withdrawal ends the replacement, so the replacement is spared exactly once. Failed with the derived withdrawal switched off.a_replacement_opened_by_a_pull_withdraws_nothingForgetstep withdrawsonly_a_replacing_mirror_of_this_volume_and_machine_withdraws_with_its_forgetForgetunder an ordinary Mirror or a Move is refuseda Volume step. A replacement naming another Machine is refusedthis Server, and one for another Volumea Volume. None of the four writes anything. Then the real replacement opens with exactly one withdrawal, and its resend adds none.records_written_before_this_format_existed_read_back_with_no_volume_recordsvolumesa_device_approval_opens_the_operation_on_disk_and_a_late_or_foreign_one_is_refusedExpired, a stranger's isUnknownKey. A fresh one opens the operation, a step outside the graph is refused, and grant, end and reload giveReplayed.a_withdrawal_by_this_machine_survives_a_reloadDashboard (
pnpm exec vitest run src/modules/approval-session/volume-approval.test.ts, 6 passed). The approval-sessioncanonicalJsonencodes each workflow, typedsatisfies VolumeApprovalfrom the generated SDK, to the same bytes the Rust golden test pins.pnpm typecheckandpnpm lintpass.Also run:
cargo clippy -p ployz-core -p ployzd --all-targets -D warnings,cargo fmt --check, and the ployzd approval, restart and rule tests on the tree rebased ontomain(67 passed).Not run. E2's two live lanes (regression and a mixed trunk/head Cluster) and its perf metrics. No route reaches this code, so a Boat run exercises nothing new. The mixed-Cluster lane checks the capability, which moved to E3.
Merge Danger
Door: two-way. Nothing calls the new code. The one persistent change is an optional
volumeskey, and nothing writes one until E3.Blast Radius: none at runtime. The
Replayedrefusal text changes for restarts. Tests match only its "already ran this step" prefix.