Skip to content

Volume approval, its verifier and the Machine's operation records (E2) - #4051

Merged
nick-potts merged 3 commits into
mainfrom
t3/sec04-e2-volume-approval
Nov 10, 2026
Merged

nick-potts merged 3 commits into
mainfrom
t3/sec04-e2-volume-approval

Conversation

@nick-potts

@nick-potts nick-potts commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

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]
Loading
core/crates/ployz-core/src/authority/
├── volume.rs          # VolumeApproval, VolumeWorkflow (Move, Mirror, MirrorRemoval,
│                      #   Restore, Resize, Seed), VolumeStep, permits, verify
└── keys.rs            # Purpose::Volume: no session grant signs it
core/crates/ployzd/src/machine/local_machine/
├── volume_approval.rs # admit_volume_step, end_volume_operation, grant_volume,
│                      #   volume_grant_live, withdraw_volume, VolumeRecords
└── restart_approval.rs# Consumption.volumes, change_volumes, read_volumes

Admission, as e-plan.md orders it:

admit_volume_step(approval, volume, step)
  record = operations[approval.operation]
  if record is Ended                    -> Replayed
  if record is Open
    same body digest, same Volume, permits(this Machine, step) -> pass, no clock read
  else
    volume::verify(approval) inside its window (clock floor applies)
    if the workflow owns this Machine's copy
      end every other open owner of this Volume here
      a Move whose source is this Machine and has no decision records Resumed
    record Open, synced, before returning
  • Signer rule (decision 1, gap 1). A new volume purpose that only a Device signs, the same rule as Purpose::Rule. A browser key under a grant of every family is refused Mismatch { 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).
  • Records (gap 3). They live in the approvals log, not as ZFS marks, so verify-volume-marks does 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.
  • Owners of a copy. A Move owns the copy on its source, its target and each Mirror it keeps following. A later operation that takes over a following Mirror's copy ends the Move there.
  • A replacing Mirror withdraws inside its own opening step. Admitting the opening MirrorToForget of a Mirror approval with replace that names this Machine writes withdrawn(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 resent Forget on the open record withdraws nothing. withdraw_volume(V, M) takes no operation to spare, so a caller cannot exempt one.
  • A Move's grant to its target is bounded. Only a Snapshots scope is accepted. Standing grants go to Mirrors only.
  • Starts are their own step. VolumeStep::Start is permitted only on a Move's two Machines. A Restore never permits one.

Left for later slices, against the plan's E2 checklist

  • approval request fields and ployz.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 through ui-copy against real output.
  • Restore's snapshot, the cut record, the handoff decision and promotion. permits checks the transition only. E4 to E6 add the snapshot and receipt checks with their routes. Decision holds Resumed only. E5 adds Handed.
  • Model. No run guard changes, so ployz-verify is untouched. E3 adds the first Broken flag with its first guard.

Bets

  • Bet 6, Machine-local authority. Every record is written and read by the Machine it lives on. A withdrawal reads no clock and no other Machine's record. An open operation finishes on its own record. No lease, no barrier, no clock-based wait.
  • Bet 3, bounded imperative commands. A workflow is a fixed graph. A step outside it is refused, however valid the signature.
  • Bet 10, client first. The client signs the statement, and the daemon only verifies it.
  • Stable promise. The approvals log gains one optional volumes key, written only when a record exists, and nothing writes one until E3. No catalog row and no request field yet. Purpose gains volume, and the Replayed text no longer says "restart", because Volume steps share it.

Residuals

Named once, per the plan's "What remains", and left alone:

  • Cloud can stall a Move after the cut or the handoff and leave a Volume with no Writer. It can end a run early, send permitted steps in any order, and choose lane gens, epochs, drains and warm rounds. In this PR that includes replaying a still-valid approval that owns a copy, which ends the operation that owned it before.
  • Recipients the customer approved receive data until their Mirror is removed. Cloud can drop a removal's step on the Writer.
  • Cloud can delay a Mirror approval past another Device's removal and reinstate that approved recipient, within the approval's window of at most 15 minutes.
  • A Machine of the Cluster could forge a receipt (decision 2, E5).
  • A Restore approved while the old Writer still runs gives two Writers until Cloud demotes one (bet 6's accepted window).

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. grant now accepts only a Snapshots scope 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 of authority, 119 passed):

Test Proves
each_workflow_signs_its_golden_canonical_body One fixed canonical body per workflow, signed under ployz-authority/v1\nvolume\n
a_device_approval_admits_each_step_its_graph_names_on_each_machine A Move admits A's snapshot, cut, resume and start, B's pulls, promotion and start, and a follower's pull. It refuses the rest (a Volume step).
the_other_workflows_permit_only_their_own_steps Mirror, removal, Restore (no start), Resize (size and shrink) and Seed (source and snapshot) graphs
each_refusal_names_what_the_approval_lacks Changed Volume, another Cluster, a Machine not named, a step outside the graph, an edited body, an unknown key, a flipped signature bit, no signature, a Move to itself
the_window_bounds_the_first_step_with_clock_skew Expired past expires_at plus skew, a future issue time, a lifetime over 15 minutes
a_browser_key_under_a_grant_of_every_family_cannot_sign_a_volume_approval The grant verifies, and the approval is still refused the signer
a_signed_approval_riding_a_proposal_is_refused No proposal bundle carries one
a_field_this_machine_cannot_read_is_malformed Unknown fields and unsorted Machine lists fail closed

ployzd (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:

Test Proves
an_open_operation_finishes_after_its_window_and_an_ended_one_never_runs_again A refused first step writes nothing. An open record passes with the check returning Expired, and after a clock rollback with no check at all. Once ended it is Replayed, its grant is not live, and it cannot be re-granted.
an_open_operation_still_admits_only_its_own_body_volume_and_graph Another body under the same ID, another Volume, a step outside the graph, a recipient the workflow does not name
a_bounded_grant_serves_only_its_snapshots_and_ends_with_its_move Snapshot scope, no Standing grant for a Move's target, nothing after the end
a_withdrawal_ends_only_what_mirrored_that_volume_to_that_machine withdraw(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_move On the Mirror, its own operation and a Move it follows both end
a_withdrawal_outranks_a_grant_under_a_move_admitted_before_it A Move keeps running, but it cannot grant the withdrawn Mirror. A later Move can.
a_new_owner_of_the_copy_ends_the_old_one_and_a_source_without_a_decision_resumes Admitting a Mirror on A ends A's Move and records Resumed, and the Move is then Replayed. 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_there Review fix 1. On a following Mirror, a later Mirror operation ends the Move, and the Move's pull is then Replayed. Failed before the fix.
a_replacing_mirrors_opening_forget_withdraws_every_other_mirror_here_and_keeps_itself Review fixes 2 and 3, E6's sequence. The replacing Forget alone writes one withdrawal. The old Mirror is Replayed, the replacement's NoneToMirror pull 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_it A resent Forget adds 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_nothing Only the Forget step withdraws
only_a_replacing_mirror_of_this_volume_and_machine_withdraws_with_its_forget On an enforcing Machine with real signatures, a Forget under an ordinary Mirror or a Move is refused a Volume step. A replacement naming another Machine is refused this Server, and one for another Volume a 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_records An older log reads back, and the first record adds volumes
a_device_approval_opens_the_operation_on_disk_and_a_late_or_foreign_one_is_refused An enforcing Machine with a real Sealing Key and Device. A late approval is Expired, a stranger's is UnknownKey. A fresh one opens the operation, a step outside the graph is refused, and grant, end and reload give Replayed.
a_withdrawal_by_this_machine_survives_a_reload The withdrawal is on disk and outranks the open Move's grant

Dashboard (pnpm exec vitest run src/modules/approval-session/volume-approval.test.ts, 6 passed). The approval-session canonicalJson encodes each workflow, typed satisfies VolumeApproval from the generated SDK, to the same bytes the Rust golden test pins. pnpm typecheck and pnpm lint pass.

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 onto main (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 volumes key, and nothing writes one until E3.

Blast Radius: none at runtime. The Replayed refusal text changes for restarts. Tests match only its "already ran this step" prefix.

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1f076a44-0d49-41b1-8019-538b2506d0b8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

…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
nick-potts force-pushed the t3/sec04-e2-volume-approval branch from 184960e to 08ac63c Compare November 10, 2026 12:59
@nick-potts
nick-potts changed the base branch from t3/sec04-d4-push-deploy to main November 10, 2026 12:59
@nick-potts
nick-potts marked this pull request as ready for review November 10, 2026 13:04
@nick-potts
nick-potts enabled auto-merge November 10, 2026 13:04
@nick-potts
nick-potts added this pull request to the merge queue Nov 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Nov 10, 2026
@nick-potts
nick-potts added this pull request to the merge queue Nov 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Nov 10, 2026
@nick-potts
nick-potts added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 3a43dc2 Nov 10, 2026
31 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