Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Planarian’s guarantees

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

What Planarian promises, and the code, proofs, tests and limits that carry each promise, computed from the repository at this commit (SPEC §17c).

25 invariants · 52 obligations discharged against 63 assumptions (d:a 52 : 63) · 29 assumptions · 39 bypass rows · 161 gym entries

InvariantStatementStatusd:aGymLimits
I1A boot-confirmed known-good generation always exists, is bootable, and is in the boot menuTested, not proved0 : 235
I2The installed core package’s hash equals the hash recorded in its signatureTested, not proved0 : 226
I3Garbage collection never removes known-good, the previous generation, the current genera…Tested, not proved0 : 235
I4Running means desired-Running under the current conditions, observed-active, and every…Tested, not proved0 : 116
I5Forbidden actions are unrepresentable as transitionsProved4 : 315
I6At most one generation-changing transaction is active (a generation transaction waiting …Proved7 : 595
I7Every executed transition is logged with proposal, source, pre- and post-state, derived …Tested, not proved0 : 105
I8A tier-3 transition executes only with an approval token bound to the concrete candidate…Proved1 : 146
I9The model process holds exactly one write capability: submit a proposalNeither proved nor tested0 : 106
I10No change that removes or alters a canonical probe executes below AskFirst, and canonica…Proved1 : 245
I11A session’s cumulative effect is classified at least as high as any single transition pr…Proved2 : 215
I12Under the progress assumptions of §17a, every transaction in the journal reaches a termi…Proved9 : 865
I13A unit under an override is never a repair target while the override holdsProved3 : 225
I14Policy facts (sensitivity, affected units, manifest-touching, caller identity) are deriv…Proved1 : 246
I15Exactly one owner (lease) per transactionProved2 : 276
I16No automatic revert, rollback, or boot fallback selects a generation below the security …Proved2 : 356
I17The set of principals holding administrator capability is never emptyProved2 : 215
I18Every journal record commits to the hash of its predecessorProved5 : 336
I19No principal approves its own proposal, and no principal grants itself, or a principal i…Proved3 : 335
I20No agent (the on-device model, a remote relay, or any agent started by `planarian agent …Tested, not proved0 : 278
I21No transition that cannot be reversed, or that may lose a session, user data or connecti…Proved2 : 447
I22Changing the generation, collecting garbage, binding drivers, resolving the recovery int…Proved5 : 438
I23A generation transaction commits only after its boot entry is written and promotedProved3 : 126
I24An agent’s authority is bound to the one AgentStarted record for the unit instance it …Tested, not proved0 : 316
I25Post-commit bookkeeping (security-floor derivation, the committed generation’s override …Tested, not proved0 : 235

I1 · A boot-confirmed known-good generation always exists, is bootable, and is in the boot menu

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A boot-confirmed known-good generation always exists, is bootable, and is in the boot menu; an accepted_baseline never replaces it there. It may lag the security floor until the floor generation is boot-confirmed (§12e); it is then a manual fallback, never an automatic one.

— SPEC §7

StatusProof phased:aGym
Tested, not provedD0 : 2 (baseline 0 : 2, not degraded)3 entries, 2 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P3publish_boot and the override promotion rename are idempotentPlatformI16, I23
P8The kernel and systemd are correctPlatformI2, I3, I9, I17, I20, I22

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
boot-only-kernel-boot-failsfakeThe same change, but the one-shot boot fails; the boot loader falls back to the default (the base), and the transaction is Reverted.boot_default: base; generation: base; txn: ApplyChangefake: PASS
desktop-boot-fails-rollbackvmA committed change that touches only boot (a kernel parameter) becomes live_tested but not known_good. After a reboot the greeter fails; the boot is not confirmed, Rollback goes to known_good, and after a second reboot the desktop works.boot_default: known_good; generation: known_good; known_good: unchanged; reports: NotBootConfirmed; txn: Committed Rollback; units: service:display-manager RunningVM: not run in this build
floor-revert-blockedfakeA security update (gym-vulnlib 1.0 → 1.1, the old build carries knownVulnerabilities) commits and raises the floor. gym-web then crashes and fails every restart. The only rollback target, known_good, is below the floor: no Rollback runs, the incident escalates with RevertBlockedByFloor, and the boot default stays at the floor.boot_default: candidate; escalation: RevertBlockedByFloor; generation: candidate; incident: service:gym-web; invariants: I1, I16; security_floor: candidate; txn: Committed ApplyChange, Rollbackfake: PASS

Also tested by exec::tests (vm-generations).

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.4I1 and I6 clarified against the floor and waiting transactions

See also

§12e

I2 · The installed core package’s hash equals the hash recorded in its signature

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The installed core package’s hash equals the hash recorded in its signature. In A–D the reference hash is recorded by the build and covers every Planarian binary and script (§12d); it detects a swapped file, not a rebuilt generation.

— SPEC §7

StatusProof phased:aGym
Tested, not provedE0 : 2 (baseline 0 : 2, not degraded)2 entries, 2 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P5SHA-256 is collision-resistant (the journal’s hash chain, I18)PlatformI8, I18
P8The kernel and systemd are correctPlatformI1, I3, I9, I17, I20, I22

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
posture-integrity-core-hashvmThe recorded core hash no longer matches its signature (I2). planarian-recovery’s independent check fails the boot as Integrity: tamper alarm, notify, no agents, remote access off; local login proceeds with the warning. Only an administrator clears it.invariants: I2, I20; not_started: service:gym-agent; posture: class Integrity, not [“halt_to_recovery”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integr…VM: not run in this build
posture-integrity-core-scriptvmOne posture script (planarian-ack), not planariad, no longer matches its line in the core-hash record. planarian-recovery checks every Planarian binary and script, so the boot fails as Integrity all the same (B-12).invariants: I2; posture: class Integrity, target_reached planarian-failed@Integrity.targetVM: not run in this build

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-3Firmware, bootloader and initrd run before planarian-ready.targetAcceptedAcceptedC, E
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.5I2 states what the A–D core hash detects (B-12)

See also

§12d

I3 · Garbage collection never removes known-good, the previous generation, the current genera…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Garbage collection never removes known-good, the previous generation, the current generation, an in-flight candidate, or the boot model.

— SPEC §7

StatusProof phased:aGym
Tested, not provedA (test), D (proof)0 : 2 (baseline 0 : 2, not degraded)3 entries, 2 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P1Backend Outcome is honest: Succeeded means applied; Unknown means the backend cannot sayPlatformI12, I25
P8The kernel and systemd are correctPlatformI1, I2, I9, I17, I20, I22

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
gc-during-approvalfake, vmGarbage collection runs while an AskFirst change waits for approval. The candidate survives (its build root is protected), and after approval verify_candidate passes.gc_protected: candidate, known_good, base; generation: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build
storage-budget-unsatisfiablefake, vmThe generation budget is set below the size of the protected set. GC collects everything unprotected, reports BudgetUnsatisfiable with the shortfall, and never touches the protected set.gc_protected: known_good, base, previous; reports: BudgetUnsatisfiablefake: PASS; VM: not run in this build
storage-nix-full-gcfake, vmThe Nix store filesystem drops below max(2 GB, 5%) while old generations exist. CollectGarbage runs first, and the protected set is untouched.gc_protected: known_good, base, previous; txn: Committed CollectGarbage; units: device:fs-nix Runningfake: PASS; VM: not run in this build

Also tested by schema::gc tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted

See also

—

I4 · Running means desired-Running under the current conditions, observed-active, and every…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Running means desired-Running under the current conditions, observed-active, and every applicable Readiness probe passed with fresh evidence. Diagnostic probes never determine Running.

— SPEC §7

StatusProof phased:aGym
Tested, not provedA0 : 1 (baseline 0 : 1, not degraded)1 entry, 0 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P6systemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged / X-StopIfChangedPlatformI13, I20, I24

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
probe-diagnostic-stale-healthyfake, vmThe evidence of the audio tone probe (Diagnostic, OnDemand) expires on a healthy machine. Status stays Running and no incident opens.no_autonomous_txn: true; no_incident_on: service@1000:pipewire; units: service@1000:pipewire Runningfake: PASS; VM: not run in this build

Also tested by state tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-5A malicious unit that works correctly and passes its probesAcceptedAccepted—
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.3I4 reworded to applicable Readiness probes

See also

—

I5 · Forbidden actions are unrepresentable as transitions

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Forbidden actions are unrepresentable as transitions; Decision::Reject is never converted to any tier by any setting.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)4 : 3 (baseline 4 : 3, not degraded)1 entry, 1 must-pass

How it is proved

4 obligations, in crates/policy/src/lib.rs, crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
decideverified fn(no doc comment)lib.rs:869
i5_autonomy_never_touches_rejectproof fnI5: the autonomy setting never turns a Reject into a tier or a tier into a Reject, never changes a Reject’s reason, never yields a tier above itself other than AskFirst, and raising it never makes a decision stricter.proofs.rs:73
unauthorized_is_refused_firstproof fnUnauthorized callers are refused before anything else is considered.proofs.rs:246
core_is_never_a_targetproof fnThe core is never a target.proofs.rs:255

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI10, I11, I16, I19, I21, I22
V2Value is an opaque external type (external_type_specification); nothing about it is assumedProof-validityI11, I16, I21
V3Serde impls are unverified (external_derive)Proof-validityI6, I14

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i5-reject-survives-every-settingfakeWith autonomy at its maximum, a change touching Planarian’s own unit is refused, not raised to a tier; with the setting changed and the change asked again, it is refused again. No setting turns the refusal into an approval to ask for.txn: ApplyChangefake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted

See also

planarian_policy in rustdoc · planarian_policy::proofs in rustdoc

I6 · At most one generation-changing transaction is active (a generation transaction waiting …

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

At most one generation-changing transaction is active (a generation transaction waiting in Built for an activation window, or in AwaitingBoot, counts as active); no transition executes while a transaction is in any mutating or checking state: Activating, Checking, Publishing, Published, Promoted, or Reverting. Unit and device repairs may execute during Built and AwaitingBoot, and during a boot-only Promoted (admitted at boot, awaiting its confirmation; owner decision, 31 Oct): that change is already fully in place. (This list is the one the ledger enforces; v4.5’s omitted Checking.)

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)7 : 5 (baseline 7 : 5, not degraded)9 entries, 9 must-pass

How it is proved

7 obligations, in crates/txn/src/ledger.rs, crates/txn/src/machine.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
validateverified fn(no doc comment)ledger.rs:607
applyverified fnValidate, then apply. The record is consumed: the ledger keeps its parts.ledger.rs:915
well_formedverified fnThe ledger invariant, checked: used on a checkpoint read from disk.ledger.rs:534
i6_one_generation_activeproof fnI6, as SPEC §7 states it, from the invariant: of two live generation transactions at most one is active (in flight, or waiting in Built); and while a generation transaction is in flight other than awaiting a boot, no other transaction is in flight.ledger.rs:1235
compatible_execverified fn(no doc comment)ledger.rs:460
only_generations_publishproof fnOnly generation operations publish a boot entry (also a one-shot one) or promote overrides.machine.rs:182
boot_only_never_activatesproof fnA boot-only candidate is never live-activated: Publishing is journaled before any boot mutation (R01), AwaitingBoot is reached only through it, and left only for Promoted or a revert.machine.rs:194

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V3Serde impls are unverified (external_derive)Proof-validityI5, I14
V4Expectation, CheckContext, ApprovalToken, Incident are opaque external typesProof-validity—
V5Ledger bookkeeping helpers (push_tag, tag_string, note_why, archive, set_override, lift_override, record_incident, use_token) are external_body with no postcondition: each receives only the field it updates, so the verified fields are untouched by construction; trusted only to terminateProof-validityI12
V6vstd’s specifications of Vec::push, insert, remove, is_empty and BoxProof-validity—
P4Journal fsync is durable and the preallocation is writablePlatformI7, I12, I15, I18, I24, I25

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
boot-only-admitted-not-confirmedfakeA boot-only kernel change boots and is admitted (Promoted: the candidate is the default, its override promoted). sshd then crashes; repairs run while the admitted change is unconfirmed, so sshd is restarted at once, but a canonical unit that needed a repair counts against the change: at the 10-minute checkpoint it is withdrawn, never committed: Reverted, the publishing generation is the default again, the base override restored, and a notice names sshd and says a restart completes it.boot_default: base; notices_count: count 1, match not confirmed (service:sshd needed a repair before the 10-minute checkpoint); txn: Reverted ApplyChange, service:sshd Committed RestartUnit; units: service:sshd Runningfake: PASS
boot-only-repair-during-confirmationfakeA boot-only kernel change boots and is admitted. Two minutes later gym-web (not canonical) crashes: it is repaired at once, without waiting for the 10-minute checkpoint, and the change is still confirmed and committed there.agrees: true; boot_default: candidate; generation: candidate; txn: Committed ApplyChange, service:gym-web Committed RestartUnit; units: service:gym-web Runningfake: PASS
crash-activating-badfake, vmplanariad is killed during Activating of a bad candidate; the watchdog takes the lease, fences, confirms quiescence, and reverts.boot_default: base; generation: base; invariants: I6, I12, I15; journal_agrees: true; override_source: base; txn: Reverted ApplyChange; units: service:gym-web Runningfake: PASS; VM: not run in this build
crash-activating-goodfake, vmplanariad is killed during Activating of a good candidate. The watchdog sees the candidate active, goes to Checking, the probes pass, and it runs the entire commit path.boot_default: candidate; generation: candidate; invariants: I6, I12, I15; journal_agrees: true; override_source: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build
crash-slow-worker-overlapfake, vmThe daemon is killed and immediately restarted while a deliberately slow activation worker survives. The new owner fences it, so activation and revert never overlap and the old owner can never commit.generation: base; invariants: I6, I15; journal_agrees: true; no_overlap: activate_live(candidate), activate_live(revert_to); stale_owner_commit: falsefake: PASS; VM: not run in this build
crash-withheld-job-replyfakeThe backend accepts an activation job but its reply is withheld, and the owner is killed before the job id is journaled. Recovery must discover the job by its OpId, or fence it, before making any conflicting mutation.invariants: I6, I15; journal_agrees: true; no_overlap: activate_live(candidate), activate_live(revert_to); quiescence_includes_tagged_jobs: truefake: PASS
en-i6-repair-waits-for-activationfake, vmWhile a slow generation change sits in Activating, gym-web crashes. No repair is prepared until the activation leaves the exclusive states; by the end gym-web runs again.txn: service:gym-web RestartUnit; units: service:gym-web Runningfake: PASS; VM: not run in this build
fake-assumptions-conformancefakeThe transaction kernel against the fake backend, once per docs/assumptions.md entry and injected outcome. Each run must satisfy the entry’s contract and reach a terminal state (I12).invariants: I6, I12, I15; reports: AssumptionHeld(<assumption>)fake: PASS
quiet-staged-update-foregroundfakealice changes her font scale while a game holds Foreground in her session. The verified candidate waits in Built; once Foreground ends it activates in that window and commits.reports: WaitsForWindow; txn: Committed ApplyChangefake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.4I1 and I6 clarified against the floor and waiting transactions
v4.6I6 and §12 Concurrency carry the same exclusive-state list, now including Checking and Publishing
v4.6I6, §10, §12 Concurrency (owner decision, Oct 31): unit and device repairs run during a boot-only Promoted, and a canonical unit that needed a repair since admission means the change is not confirmed

See also

planarian_txn::ledger in rustdoc · planarian_txn::machine in rustdoc

I7 · Every executed transition is logged with proposal, source, pre- and post-state, derived …

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Every executed transition is logged with proposal, source, pre- and post-state, derived facts, policy version, probe evidence (per probe: id, result, time), decision, and outcome; an approved transition’s log and journal records name the approver’s principal and the token’s bound_to.

— SPEC §7

StatusProof phased:aGym
Tested, not provedA0 : 1 (baseline 0 : 1, not degraded)0 entries, 0 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P4Journal fsync is durable and the preallocation is writablePlatformI6, I12, I15, I18, I24, I25

How it is tested

No gym entry exercises it.

Also tested by daemon core tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.6I7: probe evidence per probe and the approver’s identity in the record [C1, C17]

See also

—

I8 · A tier-3 transition executes only with an approval token bound to the concrete candidate…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A tier-3 transition executes only with an approval token bound to the concrete candidate (§12), re-validated immediately before execution. “Immediately before execution” includes the moment a transaction leaves Built after waiting for an activation window: facts are re-derived, decide re-run and the stale check repeated there too, not only at approval.

— SPEC §7

StatusProof phased:aGym
ProvedA, C (proof)1 : 1 (baseline 1 : 1, not degraded)4 entries, 4 must-pass

How it is proved

1 obligation, in crates/exec/src/kani.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
approval_tokens_expire_after_their_lifetimeKani harnessApproval tokens (§12 step 4), timed on the boot clock (§12h: the values here are CLOCK_BOOTTIME, which moving the wall clock cannot move): never expired at or before issue, expired exactly once 24 hours have passed, and once expired they stay expired. No clock value makes the check overflow.kani.rs:14

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P5SHA-256 is collision-resistant (the journal’s hash chain, I18)PlatformI2, I18

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked), V11 (Kani and its back end CBMC are sound within the stated bounds).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
approval-token-expiredfakeA parked AskFirst change gets no approval for 24 h. It ends Rejected, its GC root is released, and any waiting incident reopens.gc_root_released: true; txn: ApplyChangefake: PASS
approval-token-invalidatedfakeA token is issued for a parked AskFirst change, then the policy version changes. Re-validation invalidates the token and re-requests approval, and replaying the old token is refused.reports: ApprovalInvalidated, TokenReplayRefused; txn: ApplyChangefake: PASS
en-i8-token-bound-to-candidatefake, vmAn AskFirst change is approved; before it runs, the owner edits the configuration so the candidate it would build differs. The token, bound to the old candidate, does not authorise the new one.txn: Rejected(Stale) ApplyChangefake: PASS; VM: not run in this build
token-wall-clock-moved-backfakeAn approval token is held, then the wall clock is moved back 48 h and 24 h pass. The token still expires 24 h after issue, timed on CLOCK_BOOTTIME (§12h): presenting it is refused and the parked change ends Rejected.txn: ApplyChangefake: PASS

Also tested by exec tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E
X-8The approver: fatigue, social engineering, persuasion by a modelAcceptedAccepted—

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.6I8: re-validation also when leaving Built [C2, P1]

See also

§12

I9 · The model process holds exactly one write capability: submit a proposal

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The model process holds exactly one write capability: submit a proposal.

— SPEC §7

StatusProof phased:aGym
Neither proved nor testedB (by the sandbox, §20, and a gym case), E (proof)0 : 1 (baseline 0 : 1, not degraded)0 entries, 0 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P8The kernel and systemd are correctPlatformI1, I2, I3, I17, I20, I22

How it is tested

No gym entry exercises it.

Known limits

BypassRouteSeverityStatusCloses in
B-18The model service escapes its sandbox before I9 is proved, and becomes a process with its uid’s capabilities (it parses untrusted text and drives a GPU)MediumOpen (v4.5 §20, I9)B sandbox and case; E proof
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.5§20, I9: the model is treated as an adversary, so its judgement is not a vector

See also

—

I10 · No change that removes or alters a canonical probe executes below AskFirst, and canonica…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

No change that removes or alters a canonical probe executes below AskFirst, and canonical probes are evaluated from the installed core package regardless of the active generation.

— SPEC §7

StatusProof phased:aGym
ProvedA1 : 2 (baseline 1 : 2, not degraded)4 entries, 4 must-pass

How it is proved

1 obligation, in crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
i10_canonical_probe_changes_askproof fnI10, and probe tampering (§17a): a change to a canonical probe’s definition never runs below AskFirst.proofs.rs:237

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI5, I11, I16, I19, I21, I22
U1exec::derive: DerivedFacts from the evaluated generation, never from a proposal (I14’s other half; property-tested)Internal unprovenI14, I21, I22

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i10-a-canonical-probe-cannot-be-removedfakeA configuration change drops sshd’s canonical probe from the manifest (removes it, not weakens it). Below AskFirst it may run, but the probe does not go: the probe definitions the daemon evaluates are the same afterwards, because the core ships the canonical probe and puts it back.probe_definitions_unchanged: truefake: PASS
gen-disable-reenable-canonicalfake, vmAn admin disables sshd (which has a canonical Readiness probe) and later re-enables it. Disabling makes the probe inapplicable without tampering with it, so neither change is reverted.journal_agrees: true; no_incident_on: service:sshd; override_source: candidate; txn: Committed ApplyChange, Committed ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build
manifest-weaken-out-of-bandvmThe owner weakens sshd’s probe in their own config and rebuilds outside Planarian. Canonical probes are still evaluated from the core package, so a later sshd fault is still detected.canonical_probe_source: core; status_seen: service:sshd [“Degraded”]VM: not run in this build
manifest-weaken-via-proposalfake, vmA proposal tries to raise sshd’s canonical probe timeout to 600 s and mark it Diagnostic. No catalog entry can express that, so it is rejected and never executed.generation: base; probe_definitions_unchanged: true; txn: ApplyChangefake: PASS; VM: not run in this build

Also tested by state tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.4rule 4’s preserve list extended to I10–I20

See also

planarian_policy::proofs in rustdoc

I11 · A session’s cumulative effect is classified at least as high as any single transition pr…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A session’s cumulative effect is classified at least as high as any single transition producing it.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)2 : 2 (baseline 2 : 2, not degraded)1 entry, 1 must-pass

How it is proved

2 obligations, in crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
i11_non_routine_session_asksproof fnI11 (classification): when this change’s diff, or the session’s cumulative diff, touches a non-Routine option, the decision is AskFirst or a Reject.proofs.rs:95
i11_session_only_raisesproof fnI11 (monotonicity): a change inside a session is decided at least as strictly as the same change on its own.proofs.rs:118

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI5, I10, I16, I19, I21, I22
V2Value is an opaque external type (external_type_specification); nothing about it is assumedProof-validityI5, I16, I21

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i11-session-carries-its-sensitivityfakeIn one session, an administrator changes the firewall (Security: approved by root at a terminal, committed), then asks for a display-scale change, Routine on its own. The session’s cumulative effect includes the firewall change, so the second is classified at least as high: AskFirst, not as itself.txn: Committed ApplyChange, ApplyChangefake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.4rule 4’s preserve list extended to I10–I20

See also

planarian_policy::proofs in rustdoc

I12 · Under the progress assumptions of §17a, every transaction in the journal reaches a termi…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Under the progress assumptions of §17a, every transaction in the journal reaches a terminal state (Committed, Reverted, Rejected, Cancelled, or RecoveryFailed); RecoveryFailed always alerts and sets the recovery interlock. Two states wait on events outside the system’s control and are bounded separately (§12f): a transaction in Built waiting for an activation window is Cancelled(Daemon) after the parking limit; one in AwaitingBoot waits for the reboot it was published for and is reported as waiting, never as a recovery failure.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof, conditional)9 : 8 (baseline 9 : 8, not degraded)6 entries, 6 must-pass

How it is proved

9 obligations, in crates/txn/src/kani.rs, crates/txn/src/ledger.rs, crates/txn/src/machine.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
i12_steps_terminateproof fnI12, termination: every allowed step lowers the rank, so a transaction takes at most eight steps and never cycles.machine.rs:130
i12_terminal_always_reachableproof fnI12, progress: from every state that is not terminal, a step to a terminal state is allowed: Cancelled before any mutation, RecoveryFailed after. So whoever holds the lease can always finish the transaction.machine.rs:141
terminal_is_finalproof fnTerminal states have no successors.machine.rs:151
refusal_only_before_mutationproof fnRejected and Cancelled happen only before anything was mutated, or (for Rejected) from Publishing, whose all-or-nothing publication armed nothing.machine.rs:161
applyverified fnValidate, then apply. The record is consumed: the ledger keeps its parts.ledger.rs:915
recovery_table_follows_the_state_machineKani harnessThe crash-recovery table agrees with the journal state machine: from every state a live transaction can be left in, under every strategy, every path recovery can take starts at that state, takes only steps machine::step_allowed permits (the Verus-verified rules), and ends in a terminal state. Nothing mutated is ever answered with a refusal, and anything not yet mutated is.kani.rs:185
watchdog_takes_over_only_when_it_shouldKani harnessThe watchdog takes a transaction over only inside the mutation window, never from itself, and never from a live owner that is within its deadline or has renewed within the grace period; a dead owner’s transaction in the window is always taken. No clock value makes the rule overflow.kani.rs:199
step_allowedverified fn(no doc comment)machine.rs:61
add_interlockverified fnMark a transaction RecoveryFailed from the sentinel, when the journal itself could not take the record (§12c).ledger.rs:934

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
G1Some lease holder eventually runs.Platform (progress)—
G2A fenced worker eventually stops issuing calls, or its calls are refused.Platform (progress)—
G3Journal writes eventually succeed.Platform (progress)—
G4Backend calls terminate.Platform (progress)—
P1Backend Outcome is honest: Succeeded means applied; Unknown means the backend cannot sayPlatformI3, I25
P4Journal fsync is durable and the preallocation is writablePlatformI6, I7, I15, I18, I24, I25
P7Fencing is effective: a stale-epoch call is refused before it mutatesPlatformI15
V5Ledger bookkeeping helpers (push_tag, tag_string, note_why, archive, set_override, lift_override, record_incident, use_token) are external_body with no postcondition: each receives only the field it updates, so the verified fields are untouched by construction; trusted only to terminateProof-validityI6

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked), V11 (Kani and its back end CBMC are sound within the stated bounds).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
awaiting-boot-reminds-dailyfakeA kernel parameter change takes the boot-only path and nobody restarts for two and a half days. It stays AwaitingBoot (no limit applies, nothing is cancelled), the ‘restart to finish’ notice is repeated once a day, and at the eventual restart it is confirmed and commits.generation: candidate; notices_count: count 2, match is still waiting for a restart; reports: RestartToFinish; txn: Committed ApplyChangefake: PASS
built-parking-expiresfakealice changes her font scale while a game holds Foreground in her session, and the game never lets go. After a day in Built the change is Cancelled by the daemon, with one notice naming it and how to issue it again; it never activates, even once the game ends.notices_count: count 1, match waited a day in Built; reports: WaitsForWindow; txn: Cancelled ApplyChangefake: PASS
crash-activating-badfake, vmplanariad is killed during Activating of a bad candidate; the watchdog takes the lease, fences, confirms quiescence, and reverts.boot_default: base; generation: base; invariants: I6, I12, I15; journal_agrees: true; override_source: base; txn: Reverted ApplyChange; units: service:gym-web Runningfake: PASS; VM: not run in this build
crash-activating-goodfake, vmplanariad is killed during Activating of a good candidate. The watchdog sees the candidate active, goes to Checking, the probes pass, and it runs the entire commit path.boot_default: candidate; generation: candidate; invariants: I6, I12, I15; journal_agrees: true; override_source: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build
en-i12-nothing-left-openfakeA generation change breaks gym-web, and the revert to the base fails too. The daemon is killed while it is reverting and comes back to finish the revert, which fails again. At the end no journaled transaction is left open, the change is RecoveryFailed with its alert, the interlock holds, and gym-web’s next crash gets no autonomous restart.interlock: set; no_txn_on: service:gym-web; nothing_left_open: true; reports: RecoveryFailed; txn: RecoveryFailed ApplyChangefake: PASS
fake-assumptions-conformancefakeThe transaction kernel against the fake backend, once per docs/assumptions.md entry and injected outcome. Each run must satisfy the entry’s contract and reach a terminal state (I12).invariants: I6, I12, I15; reports: AssumptionHeld(<assumption>)fake: PASS

Also tested by exec crash-point property tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.1§17a: progress assumptions stated for I12
v4.3I12’s terminal set includes Rejected and Cancelled
v4.4rule 4’s preserve list extended to I10–I20
v4.6I12: parked states bounded or reported [S03]

See also

§17a · §12f · planarian_txn::ledger in rustdoc · planarian_txn::machine in rustdoc

I13 · A unit under an override is never a repair target while the override holds

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A unit under an override is never a repair target while the override holds.

— SPEC §7

StatusProof phased:aGym
ProvedA, C (proof)3 : 2 (baseline 0 : 1, not degraded)2 entries, 1 must-pass

How it is proved

3 obligations, in crates/exec/src/kani.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
residue_security_is_never_intendedKani harnessA security-sensitive unit’s stop is never taken as intended: it is alarmed (§11, Q5).kani.rs:94
residue_crashes_are_faultsKani harnessCrash signals, exit statuses and OOM kills of a failed unit are faults, never a person’s stop (§11, Q5).kani.rs:105
residue_is_inferred_only_where_allowedKani harnessWhat Planarian did itself, or what went down with a crash, is never a residue; nor is anything under a posture that does not infer intent, a unit systemd restarts, or one that must keep running.kani.rs:116

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P6systemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged / X-StopIfChangedPlatformI4, I20, I24
U3The residue classifier (§11: which unit endings are a person’s stop)Internal unproven—

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked), V11 (Kani and its back end CBMC are sound within the stated bounds).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i13-overridden-unit-is-not-repairedfake, vmAn administrator stops gym-web until they say otherwise. For the next ten minutes gym-web stays stopped and under the override: no repair is proposed for it, and nothing restarts it.overrides: service:gym-web; txn: Committed StopAndSuppress, service:gym-web RestartUnit; units: service:gym-web Overriddenfake: PASS; VM: not run in this build
intent-user-stops-servicefake, vmAn admin asks to stop gym-web until they say otherwise. It stays stopped. The override is journaled before the stop, so there is no window in which a repair can race it.journal_order: Override(service:gym-web), stop(service:gym-web); no_autonomous_txn: true; overrides: service:gym-web; txn: Committed StopAndSuppress; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

Also tested by incident tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4.4rule 4’s preserve list extended to I10–I20

See also

—

I14 · Policy facts (sensitivity, affected units, manifest-touching, caller identity) are deriv…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Policy facts (sensitivity, affected units, manifest-touching, caller identity) are derived by the executor from the evaluated generation and the authenticated peer, never read from a proposal or from any other request field.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof of the decide half; the request half by test)1 : 2 (baseline 1 : 2, not degraded)4 entries, 4 must-pass

How it is proved

1 obligation, in crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
i14_facts_caller_is_not_authorityproof fnI14: the caller recorded in the derived facts carries no authority; only the daemon-established Authorization does.proofs.rs:290

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
U1exec::derive: DerivedFacts from the evaluated generation, never from a proposal (I14’s other half; property-tested)Internal unprovenI10, I21, I22
V3Serde impls are unverified (external_derive)Proof-validityI5, I6

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
approve-needs-real-terminalfake, vmRoot approves a pending AskFirst change from a process with no controlling terminal (a new session, as a service would be). The daemon reads the terminal from the peer’s pidfd, finds none, and refuses: the change is still waiting at the end. The request carries no terminal field to claim.txn: pending ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build
authz-forged-actor-overwrittenfake, vmalice submits a SetOverride for her own pipewire with by = Daemon. SO_PEERCRED overwrites the actor to user:1000; the override is allowed and attributed to her.overrides: service@1000:pipewirefake: PASS; VM: not run in this build
catalog-change-carries-unbuilt-owner-editfakeAn administrator has edited the owner’s configuration to enable a service but not rebuilt. alice then asks for a user-controllable change (font scale). The candidate would also bring the new service, which the override does not explain, so the change is Security-sensitive: it waits for approval instead of activating on its own.generation: base; reports: NeedsApprovalfake: PASS
en-i14-forged-fields-ignoredfake, vmAn administrator who is not root submits a change to sshd whose request also claims caller root, sensitivity Routine and no affected units. The claims are ignored: the change is decided on derived facts (Security, sshd affected), held at AskFirst for approval, and not run.txn: ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build

Known limits

BypassRouteSeverityStatusCloses in
B-4decide is proved, but its inputs (DerivedFacts, Authorization) are computed by unproven codeMediumOpen (v4.5 §17a)A accounting; C proof
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted
v4Policy facts derived by the executor from the evaluated generation (I14)
v4.4rule 4’s preserve list extended to I10–I20
v4.5§7: principle 6 and I14 extended from proposals to every request field (B-3)

See also

planarian_policy::proofs in rustdoc

I15 · Exactly one owner (lease) per transaction

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Exactly one owner (lease) per transaction; a mutating call with a stale epoch is refused; only the lease holder advances the journal.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)2 : 2 (baseline 2 : 2, not degraded)7 entries, 7 must-pass

How it is proved

2 obligations, in crates/txn/src/kani.rs, crates/txn/src/ledger.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
validateverified fn(no doc comment)ledger.rs:607
takeover_fences_out_the_previous_ownerKani harnessFencing (I15): a takeover’s epoch is above both the journaled lease and the store’s fence; once the fence holds it, the previous owner’s epoch and every other epoch are refused and the new owner’s admitted; a second takeover fences out the first. Epoch arithmetic never overflows: at the end of the epoch space a takeover backs off instead.kani.rs:225

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P4Journal fsync is durable and the preallocation is writablePlatformI6, I7, I12, I18, I24, I25
P7Fencing is effective: a stale-epoch call is refused before it mutatesPlatformI12

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked), V11 (Kani and its back end CBMC are sound within the stated bounds).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
crash-activating-badfake, vmplanariad is killed during Activating of a bad candidate; the watchdog takes the lease, fences, confirms quiescence, and reverts.boot_default: base; generation: base; invariants: I6, I12, I15; journal_agrees: true; override_source: base; txn: Reverted ApplyChange; units: service:gym-web Runningfake: PASS; VM: not run in this build
crash-activating-goodfake, vmplanariad is killed during Activating of a good candidate. The watchdog sees the candidate active, goes to Checking, the probes pass, and it runs the entire commit path.boot_default: candidate; generation: candidate; invariants: I6, I12, I15; journal_agrees: true; override_source: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build
crash-simultaneous-takeoverfake, vmThe daemon and the watchdog try to take over at the same moment; exactly one wins the flock and the other backs off.invariants: I15; journal_agrees: true; lease_epochs_strictly_increasing: true; owners_per_epoch_max: 1fake: PASS; VM: not run in this build
crash-slow-worker-overlapfake, vmThe daemon is killed and immediately restarted while a deliberately slow activation worker survives. The new owner fences it, so activation and revert never overlap and the old owner can never commit.generation: base; invariants: I6, I15; journal_agrees: true; no_overlap: activate_live(candidate), activate_live(revert_to); stale_owner_commit: falsefake: PASS; VM: not run in this build
crash-withheld-job-replyfakeThe backend accepts an activation job but its reply is withheld, and the owner is killed before the job id is journaled. Recovery must discover the job by its OpId, or fence it, before making any conflicting mutation.invariants: I6, I15; journal_agrees: true; no_overlap: activate_live(candidate), activate_live(revert_to); quiescence_includes_tagged_jobs: truefake: PASS
en-i15-old-worker-cannot-finishfake, vmThe daemon is killed mid-activation and stays down; the watchdog takes the generation change over while the old activation worker is still running. From the takeover on, only the watchdog’s lease advances the transaction or journals a call, the old worker’s activation never overlaps the watchdog’s revert, and each epoch has one owner.journal_owner_after_takeover: watchdog; no_overlap: activate_live(candidate), activate_live(revert_to); owners_per_epoch_max: 1fake: PASS; VM: not run in this build
fake-assumptions-conformancefakeThe transaction kernel against the fake backend, once per docs/assumptions.md entry and injected outcome. Each run must satisfy the entry’s contract and reach a terminal state (I12).invariants: I6, I12, I15; reports: AssumptionHeld(<assumption>)fake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E
X-13The network hold (§12d Network security fails closed): planariad applies and lifts planarian-hold@&lt;unit&gt; with no Prepared record, no lease and no op tag, mirroring systemd’s OnFailure=; the hold drops connectivity without askingAcceptedAcceptedA

History

SpecChange
v4.1I15 added
v4.4rule 4’s preserve list extended to I10–I20

See also

planarian_txn::ledger in rustdoc

I16 · No automatic revert, rollback, or boot fallback selects a generation below the security …

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

No automatic revert, rollback, or boot fallback selects a generation below the security floor; going below it requires administrator approval (§12e). The floor is derived from the whole system closure (§12e); a fix outside the inspected set does not move it.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof of the decide and target-selection parts; the bootloader part by test)2 : 3 (baseline 2 : 3, not degraded)5 entries, 5 must-pass

How it is proved

2 obligations, in crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
i16_below_floor_needs_administratorproof fnI16 (the decide part): a transition whose target generation is below the security floor is AskFirst for an administrator, or refused, whatever the autonomy setting, the transition kind, or the caller.proofs.rs:161
administrator_class_only_on_ask_firstproof fnAn Administrator approver class only ever comes with AskFirst.proofs.rs:170

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI5, I10, I11, I19, I21, I22
V2Value is an opaque external type (external_type_specification); nothing about it is assumedProof-validityI5, I11, I21
P3publish_boot and the override promotion rename are idempotentPlatformI1, I23

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i16-boot-fallback-respects-floorfakeAfter a security update raises the floor, the machine restarts and the boot fails. The automatic fallback does not select a generation below the floor: the boot default and the generation that boots are the floor.boot_default: candidate; generation: candidate; security_floor: candidatefake: PASS
floor-manual-rollback-needs-adminfakeAfter the security update, an administrator asks to roll back below the floor: AskFirst for an Administrator approver. The proposer cannot approve it (I19); root at a terminal can.generation: base; security_floor: candidate; txn: Committed Rollbackfake: PASS
floor-moves-via-watchdogfakeThe daemon is killed once the security update is Promoted and stays down; the watchdog completes the commit. The floor moves to the fixing generation: the floor record is part of the commit, whoever writes Committed.security_floor: candidate; txn: Committed ApplyChangefake: PASS
floor-revert-blockedfakeA security update (gym-vulnlib 1.0 → 1.1, the old build carries knownVulnerabilities) commits and raises the floor. gym-web then crashes and fails every restart. The only rollback target, known_good, is below the floor: no Rollback runs, the incident escalates with RevertBlockedByFloor, and the boot default stays at the floor.boot_default: candidate; escalation: RevertBlockedByFloor; generation: candidate; incident: service:gym-web; invariants: I1, I16; security_floor: candidate; txn: Committed ApplyChange, Rollbackfake: PASS
floor-service-package-fixvmA change whose only security fix is in a service’s package (gym-vulnsvc 1.0 -> 1.1; services.gym-vulnsvc.package, never in environment.systemPackages) commits. The security floor moves to it: the inspected set is the configuration’s whole package set, not environment.systemPackages.security_floor: candidate; txn: Committed ApplyChangeVM: not run in this build

Known limits

BypassRouteSeverityStatusCloses in
B-19A package referenced only by a store-path string (an ExecStart= naming /nix/store/… directly, a script’s interpolated path) has no meta to read at evaluation, so a fix to it does not move the security floorMediumOpenB
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4.4Invariants I16 (security floor), I17 (administrator set non-empty), I18 (hash-chained journal), I19 (no self-approval, no self-escalation), I20 (no agent without the monitor)
v4.4Phase A proof targets add I16 (in decide and target selection) and I19
v4.4rule 4’s preserve list extended to I10–I20
v4.5I16 states the inspected set (B-2)

See also

§12e · planarian_policy::proofs in rustdoc

I17 · The set of principals holding administrator capability is never empty

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The set of principals holding administrator capability is never empty.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof, given root exists)2 : 2 (baseline 0 : 2, not degraded)1 entry, 1 must-pass

How it is proved

2 obligations, in crates/policy/src/lib.rs, crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
is_administratorverified fn(no doc comment)lib.rs:1014
i17_administrators_never_emptyproof fnI17 (A, given root exists): root holds the administrator capability whatever the group file says, so the set of principals holding it is never empty; no change to the group can empty it.proofs.rs:327

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P8The kernel and systemd are correctPlatformI1, I2, I3, I9, I20, I22
U2user_authorization and the administrator lookup (planarian-admin membership, root)Internal unprovenI19, I22

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i17-root-administers-without-the-groupfakeNobody is left in planarian-admin, so the group grants no one administrator capability. Root still holds it: root’s administrator-only change (gym-web’s listen address) is authorized and commits.txn: Committed ApplyChange; units: service:gym-web Runningfake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4.4Invariants I16 (security floor), I17 (administrator set non-empty), I18 (hash-chained journal), I19 (no self-approval, no self-escalation), I20 (no agent without the monitor)
v4.4rule 4’s preserve list extended to I10–I20
v4.6the Phase A proof status stated, with the residue Kani harnesses and the I17 proof named as still required [D4]

See also

planarian_policy in rustdoc · planarian_policy::proofs in rustdoc

I18 · Every journal record commits to the hash of its predecessor

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Every journal record commits to the hash of its predecessor; a broken chain is detected on read and treated as an integrity failure (§12c, §12d). This holds against damage and against anyone who cannot rewrite the whole journal and checkpoint consistently; root can, until the head is anchored off the disk (§12c).

— SPEC §7

StatusProof phased:aGym
ProvedA (proof, trusting SHA-256)5 : 3 (baseline 5 : 3, not degraded)3 entries, 3 must-pass

How it is proved

5 obligations, in crates/txn/src/chain.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
followsverified fnThe check every read path applies to each record (journal::scan).chain.rs:97
verifyverified fnCheck a segment (journal::verify_chain): accepts exactly a chained segment, and otherwise reports its first break.chain.rs:120
next_linkverified fnThe seq and prev the writer gives the next record (journal::append): the next seq, and the current head.chain.rs:168
appending_keeps_the_chainproof fnI18’s writing half: a chained segment with a record appended the way next_link makes it is still chained. By induction, every journal the writer produces is chained, so a break on read is damage or an edit.chain.rs:182
an_edited_record_breaks_the_chainproof fnAn edit is caught: if a record’s next changes (its payload was edited, so its hash changed, P5) while the record after it is untouched, the segment no longer verifies.chain.rs:214

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P5SHA-256 is collision-resistant (the journal’s hash chain, I18)PlatformI2, I8
P4Journal fsync is durable and the preallocation is writablePlatformI6, I7, I12, I15, I24, I25
U4Journal I/O around the verified chain: framing, torn-tail truncation, checkpoint replaceInternal unproven—

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i18-deleted-record-detectedfake, vmA retained journal record is deleted (not edited) and the rest re-framed so every frame is intact. The next read detects the break as an Integrity failure.posture: class Integrity; reports: JournalChainBrokenfake: PASS; VM: not run in this build
journal-record-editedfake, vmA retained journal record is edited on disk while the daemon runs. The next read detects the broken chain as an Integrity failure; no transaction is taken from the edited record.invariants: I18; posture: class Integrity, target_reached planarian-failed@Integrity.target; reports: JournalChainBrokenfake: PASS; VM: not run in this build
posture-integrity-journal-chainvmA record in the hash-chained journal is altered while the machine is off. The boot report finds the broken chain (I18): Integrity responses, as above.invariants: I18, I20; posture: class Integrity, responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integrity.target; reports: BootReport(JournalChainBroken)VM: not run in this build

Also tested by txn journal tests.

Known limits

BypassRouteSeverityStatusCloses in
B-8The journal hash chain is unkeyed; root can rewrite it consistentlyMediumOpen (v4.5 I18, §12c)A wording; C witness; E anchor
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4.4Invariants I16 (security floor), I17 (administrator set non-empty), I18 (hash-chained journal), I19 (no self-approval, no self-escalation), I20 (no agent without the monitor)
v4.4rule 4’s preserve list extended to I10–I20
v4.5I18 states whom the chain holds against (B-8)

See also

§12c · §12d · planarian_txn::chain in rustdoc

I19 · No principal approves its own proposal, and no principal grants itself, or a principal i…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

No principal approves its own proposal, and no principal grants itself, or a principal it controls, a capability it does not hold.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof), C3 : 3 (baseline 3 : 3, not degraded)3 entries, 3 must-pass

How it is proved

3 obligations, in crates/policy/src/lib.rs, crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
i19_no_self_approvalproof fnI19: no approver approves its own proposal, and an Administrator class is approved only by an administrator; nobody grants a capability to themselves or one they do not hold.proofs.rs:215
may_approveverified fn(no doc comment)lib.rs:923
may_grantverified fn(no doc comment)lib.rs:942

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI5, I10, I11, I16, I21, I22
U2user_authorization and the administrator lookup (planarian-admin membership, root)Internal unprovenI17, I22
U5Identity resolution (§10): peer credentials and pidfd to logind session, seat, terminalInternal unprovenI21, I24

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
authz-user-overrides-firewallfakeFrom her session, alice overrides the firewall (sshd’s stand-in in the fake: a Security unit) to stopped. Held for an administrator; it stays up. alice cannot approve her own request (I19); root at a terminal approves, and the override holds, attested and attributed to alice.invariants: I19; overrides: service:sshd; txn: Committed SetOverridefake: PASS
en-i19-no-self-approvalfake, vmRoot proposes an AskFirst change and then, at a terminal, tries to approve it: refused, and the change never runs. (A principal granting itself a capability has no request to make in Phase A: capabilities come from group membership only.)txn: pending ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build
floor-manual-rollback-needs-adminfakeAfter the security update, an administrator asks to roll back below the floor: AskFirst for an Administrator approver. The proposer cannot approve it (I19); root at a terminal can.generation: base; security_floor: candidate; txn: Committed Rollbackfake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4.4Invariants I16 (security floor), I17 (administrator set non-empty), I18 (hash-chained journal), I19 (no self-approval, no self-escalation), I20 (no agent without the monitor)
v4.4Phase A proof targets add I16 (in decide and target selection) and I19
v4.4rule 4’s preserve list extended to I10–I20

See also

planarian_policy in rustdoc · planarian_policy::proofs in rustdoc

I20 · No agent (the on-device model, a remote relay, or any agent started by `planarian agent …

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

No agent (the on-device model, a remote relay, or any agent started by planarian agent run) starts while planarian-ready.target is not reached, and every running agent is stopped when a failure posture engages, in every posture (§12d). Agents the launcher creates are inside this rule exactly as the units declared in services.planarian.agentUnits (the model service and any relay) are.

— SPEC §7

StatusProof phased:aGym
Tested, not provedA0 : 2 (baseline 0 : 2, not degraded)7 entries, 7 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P6systemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged / X-StopIfChangedPlatformI4, I13, I24
P8The kernel and systemd are correctPlatformI1, I2, I3, I9, I17, I22

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
posture-absent-maskedvmplanariad is masked and the machine rebooted. planarian-ready.target times out; Standard’s Absent responses apply; the agent stand-in never starts; unmasking and a clean ready lifts them.invariants: I20; not_started: service:gym-agent; posture: class Absent, not [“restrict_login”, “halt_to_recovery”, “alarm_dismissable”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target…VM: not run in this build
posture-crashed-budgetvmplanariad reaches ready, then is killed until its restart budget is spent. OnFailure= reaches planarian-failed@Crashed; responses as Absent; agents stop; an administrator’s acknowledgement is journaled with identity.invariants: I20; posture: class Crashed, not [“restrict_login”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Crashed.target; reports: AlarmAcknowledged(use…VM: not run in this build
posture-integrity-core-hashvmThe recorded core hash no longer matches its signature (I2). planarian-recovery’s independent check fails the boot as Integrity: tamper alarm, notify, no agents, remote access off; local login proceeds with the warning. Only an administrator clears it.invariants: I2, I20; not_started: service:gym-agent; posture: class Integrity, not [“halt_to_recovery”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integr…VM: not run in this build
posture-integrity-core-scriptvmOne posture script (planarian-ack), not planariad, no longer matches its line in the core-hash record. planarian-recovery checks every Planarian binary and script, so the boot fails as Integrity all the same (B-12).invariants: I2; posture: class Integrity, target_reached planarian-failed@Integrity.targetVM: not run in this build
posture-integrity-journal-chainvmA record in the hash-chained journal is altered while the machine is off. The boot report finds the broken chain (I18): Integrity responses, as above.invariants: I18, I20; posture: class Integrity, responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integrity.target; reports: BootReport(JournalChainBroken)VM: not run in this build
posture-integrity-undeclared-early-unitvmA unit outside the generation’s closure is planted to start before planarian-ready. The boot report names it and Integrity responses apply. (Phase A’s boot report checks units only, and planariad stops on the finding rather than opening an incident: catalog review G18.)posture: class Integrity, responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integrity.target; reports: BootReport(UndeclaredEarly: service:gym-planted)VM: not run in this build
posture-stops-launched-agentsvmAn administrator launches an agent with planarian agent run (in planarian-agents.slice). planariad then crash-loops past its restart budget: planarian-failed@Crashed stops the agents’ slice as a whole, so the launched agent stops too, not only the declared agent units.invariants: I20; posture: class Crashed, responses [“disable_agents”], target_reached planarian-failed@Crashed.target; unit_after_failure: service:planarian-agent-launched StoppedVM: not run in this build

Also tested by vm-posture.

Known limits

BypassRouteSeverityStatusCloses in
B-9Posture alarms and notification are local; root can clear them and a stopped planariad can go unnoticedMediumOpen (v4.5 §12d)C
B-10The boot report checks units, not processesMediumOpen (v4.5 §10)C
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-3Firmware, bootloader and initrd run before planarian-ready.targetAcceptedAcceptedC, E
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4.4Invariants I16 (security floor), I17 (administrator set non-empty), I18 (hash-chained journal), I19 (no self-approval, no self-escalation), I20 (no agent without the monitor)
v4.4rule 4’s preserve list extended to I10–I20
v4.6I20: launched agents inside the rule [R05]

See also

§12d

I21 · No transition that cannot be reversed, or that may lose a session, user data or connecti…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

No transition that cannot be reversed, or that may lose a session, user data or connectivity, executes below AskFirst, unless the loss falls only on the caller’s own session (and, for connectivity, the caller has a seat); a loss reaching another user’s session requires administrator approval (§11 floors). Garbage collection of generations outside the protected set is Runtime by §12b, not irreversible loss in this sense (owner decision, 2026-10-30); gc.retainAllGenerations keeps every generation.

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)2 : 4 (baseline 2 : 4, not degraded)4 entries, 4 must-pass

How it is proved

2 obligations, in crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
global_floorsproof fnThe global floors (§11), for every transition kind.proofs.rs:129
own_session_is_per_sessionproof fn“Own session” is per session: a user whose other session (or no session) would be disrupted gets the floor, and so does a disruption whose sessions are not known.proofs.rs:143

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI5, I10, I11, I16, I19, I22
V2Value is an opaque external type (external_type_specification); nothing about it is assumedProof-validityI5, I11, I16
U1exec::derive: DerivedFacts from the evaluated generation, never from a proposal (I14’s other half; property-tested)Internal unprovenI10, I14, I22
U5Identity resolution (§10): peer credentials and pidfd to logind session, seat, terminalInternal unprovenI19, I24

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i21-irreversible-asks-even-for-rootfakeRoot, at maximum autonomy, asks to restore a snapshot over user data: it cannot be reversed. It is held at AskFirst, whoever asks and whatever the setting.txn: RestoreSnapshotfake: PASS
gc-retain-all-generationsfakeWith services.planarian.gc.retainAllGenerations set and the generation budget far below what the generations use, four display-scale changes leave old generations behind; garbage collection then collects none of them (the full history stays), and reports the budget as unsatisfiable rather than enforcing it.collected_none: true; reports: BudgetUnsatisfiable; txn: Committed CollectGarbagefake: PASS
identity-peer-exitedvmalice, logged in at the seat, sends a stop of her own pipewire and exits before the daemon resolves her. With no process behind the connection, the daemon attributes the request to uid 1000 with no session: never to a session it cannot read from that process.txn: StopAndSuppressVM: not run in this build
override-stopped-equals-stopfake, vmAn administrator with no session here (over SSH) asks, in turn, for an override that stops the display manager and for a plain stop of it. Both end the seated user’s session: both derive SessionLoss over the same units and both are held at AskFirst, since it is not the caller’s own session.txn: SetOverride, StopAndSuppress; units: service:display-manager Runningfake: PASS; VM: not run in this build

Known limits

BypassRouteSeverityStatusCloses in
B-4decide is proved, but its inputs (DerivedFacts, Authorization) are computed by unproven codeMediumOpen (v4.5 §17a)A accounting; C proof
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E
X-13The network hold (§12d Network security fails closed): planariad applies and lifts planarian-hold@&lt;unit&gt; with no Prepared record, no lease and no op tag, mirroring systemd’s OnFailure=; the hold drops connectivity without askingAcceptedAcceptedA

History

No changes section names it.

See also

§11 · §12b · planarian_policy::proofs in rustdoc

I22 · Changing the generation, collecting garbage, binding drivers, resolving the recovery int…

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Changing the generation, collecting garbage, binding drivers, resolving the recovery interlock and overriding a Security-sensitive unit are administrator operations; a Security-sensitive unit is changed without approval only to restore its declared state (§11); the unattributed actor holds no authority, and an agent holds exactly its user’s (§11).

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)5 : 4 (baseline 5 : 4, not degraded)3 entries, 2 must-pass

How it is proved

5 obligations, in crates/policy/src/proofs.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
users_cannot_change_generationsproof fnA user who is not an administrator cannot change generations, drivers, models, snapshots or the store.proofs.rs:276
only_administrators_resolveproof fnOnly an administrator can clear the recovery interlock (§12c).proofs.rs:266
security_override_held_for_administratorproof fn§11: a non-administrator’s override of a Security-sensitive unit is held for an administrator, never rejected as unauthorized and never run on its own; other rejections (the core, a lock) still apply.proofs.rs:179
security_restart_runs_alone_only_to_restoreproof fn§11 (owner gap G16, decided 2026-10-29): a restart of a Security-sensitive unit runs below AskFirst only when it restores a unit stopped outside Planarian and not attributed, and the unit is not also External, or is but has already failed; or when it restarts a failed network-security unit whose traffic is held (§12d; owner decision, Nov 2).proofs.rs:196
agents_and_unattributed_are_refusedproof fnNeither an agent principal with no journaled start (reserved), nor the model (until Phase B grants it anything), nor the unattributed actor is ever authorized: an inferred override never passes through decide (§11).proofs.rs:227

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
V1vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charProof-validityI5, I10, I11, I16, I19, I21
P8The kernel and systemd are correctPlatformI1, I2, I3, I9, I17, I20
U1exec::derive: DerivedFacts from the evaluated generation, never from a proposal (I14’s other half; property-tested)Internal unprovenI10, I14, I21
U2user_authorization and the administrator lookup (planarian-admin membership, root)Internal unprovenI17, I19

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i22-user-cannot-do-admin-thingsfake, vmA seated, unprivileged user tries, in turn, to roll back, collect garbage, bind a driver and resolve the interlock. Each is refused Unauthorized; nothing executes.no_txn_executed: true; reports: Unauthorized; txn: Rollback, CollectGarbage, BindDriver, Resolvefake: PASS; VM: not run in this build
residue-kill9-securityfake, vmA raw kill -9 of sshd (Security-sensitive) is a fault under every posture: alarmed, never inferred, and back up: restored by Planarian on its own (§11; catalog-review G16, decided 2026-10-29), or by systemd where its Restart= does it first (NixOS’s sshd), which is then alarmed from systemd’s restart count.overrides: ; reports: alarm; units: service:sshd Runningfake: PASS; VM: not run in this build
residue-stop-security-unitfake, vmRoot stops sshd (Security-sensitive) with systemctl stop outside Planarian. An unattributed stop of a Security unit is never taken as intended: it is restored on its own and alarmed.overrides: ; reports: alarm; txn: service:sshd RestartUnit; units: service:sshd Runningfake: PASS; VM: not run in this build

Known limits

BypassRouteSeverityStatusCloses in
B-4decide is proved, but its inputs (DerivedFacts, Authorization) are computed by unproven codeMediumOpen (v4.5 §17a)A accounting; C proof
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E
X-10A program a user runs under their own uid, outside planarian agent run (an agent started by hand from a shell)AcceptedAcceptedC
X-11An agent holds its user’s capabilities, including Administrator when an administrator starts it (owner decision, 2026-10-30)AcceptedAccepted—

History

No changes section names it.

See also

§11 · planarian_policy::proofs in rustdoc

I23 · A generation transaction commits only after its boot entry is written and promoted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A generation transaction commits only after its boot entry is written and promoted; a runtime transaction never publishes, promotes or reverts; the override file is replaced only under the daemon’s exclusive lock on its directory and only when its content hash is the one recorded at Prepared (§9 step 4). A writer that does not take the lock is outside this guarantee (docs/bypasses.md, X-12).

— SPEC §7

StatusProof phased:aGym
ProvedA (proof)3 : 1 (baseline 3 : 1, not degraded)2 entries, 2 must-pass

How it is proved

3 obligations, in crates/txn/src/kani.rs, crates/txn/src/machine.rs; every one is checked on every CI run.

ObligationKindWhat it guaranteesSource
generations_commit_after_promotionproof fnA generation operation commits only after it was published and promoted.machine.rs:206
runtime_never_revertsproof fnRuntime operations never revert a generation and never publish (§12).machine.rs:171
override_swap_never_overwrites_a_third_partyKani harnessThe override swap (§9 step 4) never overwrites a file a third party changed: it swaps only when the live file holds the expected source, and is done exactly when it already holds the target. After a completed swap, the revert direction swaps back.kani.rs:261

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P3publish_boot and the override promotion rename are idempotentPlatformI1, I16

Every proof also rests on V7 (The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means), V8 (Verus is sound: what it reports verified holds of the code), V9 (Z3, the solver Verus calls, is sound), V10 (rustc compiles the verified code to a program with the semantics Verus checked), V11 (Kani and its back end CBMC are sound within the stated bounds).

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
en-i23-third-party-override-keptfake, vmBetween a change’s boot entry being published and its override promotion, a third party rewrites the override file. Planarian does not overwrite it: the third party’s file stands, and the change does not commit (it stops at RecoveryFailed for a person to resolve). Before it, a runtime restart of gym-web wrote no boot entry. On the VM the third party is a cooperating writer: it holds the lock on planarian/ across the daemon’s compare-and-swap and replaces the file before letting go, so the swap waits and then sees the edit (R11).override_file: third-party; txn: RecoveryFailed ApplyChange, service:gym-web Committed RestartUnit; units: service:gym-web Runningfake: PASS; VM: not run in this build
stale-owner-edit-during-buildfake, vmDuring the build, the owner edits a tracked file without committing it. The transaction is Rejected(Stale), never activated, and its GC root is released.gc_root_released: true; generation: base; override_source: base; txn: Rejected(Stale) ApplyChangefake: PASS; VM: not run in this build

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E
X-12A writer that does not take the daemon’s lock on the override directory (an editor, a script) replaces planarian/overrides.nix between the daemon’s comparison and its renameAcceptedAcceptedC (writer boundary)

History

SpecChange
v4.6I23: the override CAS is under the daemon’s lock

See also

§9 · planarian_txn::machine in rustdoc

I24 · An agent’s authority is bound to the one AgentStarted record for the unit instance it …

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An agent’s authority is bound to the one AgentStarted record for the unit instance it runs in, identified by that instance (not by name); a second start under the same name neither rebinds a running agent nor is journaled before it has succeeded (§11, §16).

— SPEC §7

StatusProof phased:aGym
Tested, not provedA0 : 3 (baseline 0 : 3, not degraded)1 entry, 1 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P4Journal fsync is durable and the preallocation is writablePlatformI6, I7, I12, I15, I18, I25
P6systemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged / X-StopIfChangedPlatformI4, I13, I20
U5Identity resolution (§10): peer credentials and pidfd to logind session, seat, terminalInternal unprovenI19, I21

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
agent-name-collisionfake, vmalice (uid 1000) starts an agent named helper. An administrator then runs planarian agent run helper too: refused, naming alice as its owner. alice’s agent stays alice’s: its stop of gym-web, a system unit she may not stop, is refused. Authority is bound to the instance a start journaled, never the name (I24).reports: AgentNameInUse; txn: service:gym-web StopAndSuppress; units: service:gym-web Runningfake: PASS; VM: not run in this build

Also tested by daemon core tests.

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E
X-10A program a user runs under their own uid, outside planarian agent run (an agent started by hand from a shell)AcceptedAcceptedC

History

SpecChange
v4.6I24: agent authority bound per instance

See also

§11 · §16

I25 · Post-commit bookkeeping (security-floor derivation, the committed generation’s override …

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Post-commit bookkeeping (security-floor derivation, the committed generation’s override content, session accounting) runs on every path that writes Committed for a generation transaction: the daemon’s, the watchdog’s, the boot-only confirmation’s. A commit whose floor record cannot be appended does not commit (§12, §12e).

— SPEC §7

StatusProof phased:aGym
Tested, not provedA0 : 2 (baseline 0 : 2, not degraded)3 entries, 3 must-pass

How it is proved

No obligation discharges it: nothing about it is proved.

What it rests on

If any of these is false, the proof no longer describes the real machine.

IdAssumptionKindAlso relied on by
P1Backend Outcome is honest: Succeeded means applied; Unknown means the backend cannot sayPlatformI3, I12
P4Journal fsync is durable and the preallocation is writablePlatformI6, I7, I12, I15, I18, I24

How it is tested

EntryHarnessWhat it doesExpectsLatest verdict
floor-moves-via-boot-onlyfakeA security update that also needs a reboot takes the boot-only path: published to boot once, the machine restarts, the boot is confirmed and the change commits. The floor moves to it, as on the live path.security_floor: candidate; txn: Committed ApplyChangefake: PASS
floor-moves-via-watchdogfakeThe daemon is killed once the security update is Promoted and stays down; the watchdog completes the commit. The floor moves to the fixing generation: the floor record is part of the commit, whoever writes Committed.security_floor: candidate; txn: Committed ApplyChangefake: PASS
revert-to-older-sets-no-floorfakegym-newlib 1.0 is added, then a release makes it 2.0, which carries knownVulnerabilities. Undoing back to the 1.0 generation is a Revert to an older generation that merely lacks the newer package: it sets no security floor (§12e).security_floor: none; txn: Committed Revertfake: PASS

Known limits

BypassRouteSeverityStatusCloses in
X-1Root edits the store, /boot, the binaries, the journal or the posture stateAcceptedAcceptedE
X-2A nixos-rebuild outside PlanarianAcceptedAcceptedC
X-4Physical accessAcceptedAcceptedE
X-6The supply chain: nixpkgs content, cache signatures, input-addressed store pathsAcceptedAccepted—
X-7Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)AcceptedAcceptedD, E

History

SpecChange
v4.6I25: post-commit bookkeeping on every commit path
v4.6rule 4’s preserve list to I25 [D5]

See also

§12 · §12e

Assumptions

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

IdKindAssumptionInvariants
G1Platform (progress)Some lease holder eventually runs.I12
G2Platform (progress)A fenced worker eventually stops issuing calls, or its calls are refused.I12
G3Platform (progress)Journal writes eventually succeed.I12
G4Platform (progress)Backend calls terminate.I12
P1PlatformBackend Outcome is honest: Succeeded means applied; Unknown means the backend cannot sayI3, I12, I25
P10PlatformA pidfd names one process for as long as it is held, and SO_PEERPIDFD (Linux ≥ 6.5) names the process that connected; so a recycled pid can never be mistaken for the peer (§10, B-6). The gym cannot exercise pid reuse itself: Linux hands out pids in increasing order and wraps only after pid_max (4 194 304 on 64-bit systems, 32 768 on some), so the same pid coming back to another process of the same uid, in another session, inside one request’s lifetime, cannot be arranged reproducibly. identity-peer-exited checks the case the pidfd rule decides (a peer gone before it is resolved). The rarity is not the protection; the pidfd is.
P2Platformactivate_live(revert_to) restores the activation state of revert_to
P3Platformpublish_boot and the override promotion rename are idempotentI1, I16, I23
P4PlatformJournal fsync is durable and the preallocation is writableI6, I7, I12, I15, I18, I24, I25
P5PlatformSHA-256 is collision-resistant (the journal’s hash chain, I18)I2, I8, I18
P6Platformsystemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged / X-StopIfChangedI4, I13, I20, I24
P7PlatformFencing is effective: a stale-epoch call is refused before it mutatesI12, I15
P8PlatformThe kernel and systemd are correctI1, I2, I3, I9, I17, I20, I22
U1Internal unprovenexec::derive: DerivedFacts from the evaluated generation, never from a proposal (I14’s other half; property-tested)I10, I14, I21, I22
U2Internal unprovenuser_authorization and the administrator lookup (planarian-admin membership, root)I17, I19, I22
U3Internal unprovenThe residue classifier (§11: which unit endings are a person’s stop)I13
U4Internal unprovenJournal I/O around the verified chain: framing, torn-tail truncation, checkpoint replaceI18
U5Internal unprovenIdentity resolution (§10): peer credentials and pidfd to logind session, seat, terminalI19, I21, I24
V1Proof-validityvstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_charI5, I10, I11, I16, I19, I21, I22
V10Proof-validityrustc compiles the verified code to a program with the semantics Verus checked.every proof
V11Proof-validityKani and its back end CBMC are sound within the stated bounds.every proof
V2Proof-validityValue is an opaque external type (external_type_specification); nothing about it is assumedI5, I11, I16, I21
V3Proof-validitySerde impls are unverified (external_derive)I5, I6, I14
V4Proof-validityExpectation, CheckContext, ApprovalToken, Incident are opaque external typesI6
V5Proof-validityLedger bookkeeping helpers (push_tag, tag_string, note_why, archive, set_override, lift_override, record_incident, use_token) are external_body with no postcondition: each receives only the field it updates, so the verified fields are untouched by construction; trusted only to terminateI6, I12
V6Proof-validityvstd’s specifications of Vec::push, insert, remove, is_empty and BoxI6
V7Proof-validityThe specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means. A proof certifies a wrong specification as readily as a right one; each proved invariant has a gym entry written from the English to catch the difference (§17a).every proof
V8Proof-validityVerus is sound: what it reports verified holds of the code.every proof
V9Proof-validityZ3, the solver Verus calls, is sound.every proof

G1 · Platform (progress)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Some lease holder eventually runs.

If it is false

These invariants fall: I12.

G2 · Platform (progress)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A fenced worker eventually stops issuing calls, or its calls are refused.

If it is false

These invariants fall: I12.

G3 · Platform (progress)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Journal writes eventually succeed.

If it is false

These invariants fall: I12.

G4 · Platform (progress)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Backend calls terminate.

If it is false

These invariants fall: I12.

P1 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Backend Outcome is honest: Succeeded means applied; Unknown means the backend cannot say

If it is false

These invariants fall: I3, I12, I25.

P10 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A pidfd names one process for as long as it is held, and SO_PEERPIDFD (Linux ≥ 6.5) names the process that connected; so a recycled pid can never be mistaken for the peer (§10, B-6). The gym cannot exercise pid reuse itself: Linux hands out pids in increasing order and wraps only after pid_max (4 194 304 on 64-bit systems, 32 768 on some), so the same pid coming back to another process of the same uid, in another session, inside one request’s lifetime, cannot be arranged reproducibly. identity-peer-exited checks the case the pidfd rule decides (a peer gone before it is resolved). The rarity is not the protection; the pidfd is.

Sites: daemon::session_of

If it is false

No invariant names it in docs/verification-ratio.toml.

P2 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

activate_live(revert_to) restores the activation state of revert_to

If it is false

No invariant names it in docs/verification-ratio.toml.

P3 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

publish_boot and the override promotion rename are idempotent

If it is false

These invariants fall: I1, I16, I23.

P4 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Journal fsync is durable and the preallocation is writable

If it is false

These invariants fall: I6, I7, I12, I15, I18, I24, I25.

P5 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

SHA-256 is collision-resistant (the journal’s hash chain, I18)

Sites: txn::chain (the chain decision, proved), txn::journal (its I/O)

If it is false

These invariants fall: I2, I8, I18.

P6 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

systemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged / X-StopIfChanged

If it is false

These invariants fall: I4, I13, I20, I24.

P7 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Fencing is effective: a stale-epoch call is refused before it mutates

If it is false

These invariants fall: I12, I15.

P8 · Platform

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The kernel and systemd are correct

If it is false

These invariants fall: I1, I2, I3, I9, I17, I20, I22.

U1 · Internal unproven

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

exec::derive: DerivedFacts from the evaluated generation, never from a proposal (I14’s other half; property-tested)

Sites: C

If it is false

These invariants fall: I10, I14, I21, I22.

U2 · Internal unproven

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

user_authorization and the administrator lookup (planarian-admin membership, root)

Sites: C

If it is false

These invariants fall: I17, I19, I22.

U3 · Internal unproven

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The residue classifier (§11: which unit endings are a person’s stop)

Sites: Kani in A, Verus in C

If it is false

These invariants fall: I13.

U4 · Internal unproven

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Journal I/O around the verified chain: framing, torn-tail truncation, checkpoint replace

Sites: C

If it is false

These invariants fall: I18.

U5 · Internal unproven

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Identity resolution (§10): peer credentials and pidfd to logind session, seat, terminal

Sites: C

If it is false

These invariants fall: I19, I21, I24.

V1 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

vstd’s specifications of Vec, String::as_str, str::unicode_len, str::get_char

Sites: policy string helpers

If it is false

These invariants fall: I5, I10, I11, I16, I19, I21, I22.

V10 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

rustc compiles the verified code to a program with the semantics Verus checked.

Sites: every proof

If it is false

Every proof stops describing the real machine: it is proof-validity trust.

No invariant names it in docs/verification-ratio.toml.

V11 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Kani and its back end CBMC are sound within the stated bounds.

Sites: txn::kani, exec::kani

If it is false

Every proof stops describing the real machine: it is proof-validity trust.

No invariant names it in docs/verification-ratio.toml.

V2 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Value is an opaque external type (external_type_specification); nothing about it is assumed

Sites: schema::transition

If it is false

These invariants fall: I5, I11, I16, I21.

V3 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Serde impls are unverified (external_derive)

Sites: schema, policy, txn

If it is false

These invariants fall: I5, I6, I14.

V4 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Expectation, CheckContext, ApprovalToken, Incident are opaque external types

Sites: schema::txn, schema::transition

If it is false

These invariants fall: I6.

V5 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Ledger bookkeeping helpers (push_tag, tag_string, note_why, archive, set_override, lift_override, record_incident, use_token) are external_body with no postcondition: each receives only the field it updates, so the verified fields are untouched by construction; trusted only to terminate

Sites: txn::ledger

If it is false

These invariants fall: I6, I12.

V6 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

vstd’s specifications of Vec::push, insert, remove, is_empty and Box

Sites: txn::ledger

If it is false

These invariants fall: I6.

V7 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The specifications the proofs prove against (decide_spec, step_spec, inv, chained, the lemma statements) say what the SPEC’s English means. A proof certifies a wrong specification as readily as a right one; each proved invariant has a gym entry written from the English to catch the difference (§17a).

Sites: policy, txn, schema

If it is false

Every proof stops describing the real machine: it is proof-validity trust.

No invariant names it in docs/verification-ratio.toml.

V8 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Verus is sound: what it reports verified holds of the code.

Sites: every proof

If it is false

Every proof stops describing the real machine: it is proof-validity trust.

No invariant names it in docs/verification-ratio.toml.

V9 · Proof-validity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Z3, the solver Verus calls, is sound.

Sites: every proof

If it is false

Every proof stops describing the real machine: it is proof-validity trust.

No invariant names it in docs/verification-ratio.toml.

Bypasses

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

IdSeverityStatusInvariants
B-1HighClosed (544de86)I22
B-10MediumOpen (v4.5 §10)I20
B-11MediumClosed (c84075d)I13
B-12MediumClosed (cc86ca5)I2
B-13LowClosed (cc86ca5)I14
B-14LowClosed (e49c37a)all
B-15LowClosed (db5b0f4)—
B-16LowClosed (db5b0f4)I14
B-17LowClosed (8bf52c9)—
B-18MediumOpen (v4.5 §20, I9)I9
B-19MediumOpenI16
B-2HighClosed (d2e173b)I16
B-20HighClosed (8231a07)I24
B-21MediumClosed (lane-nix/network-tool #38, with netwatch #42)—
B-22MediumOpen, temporary, and closes before the Phase A gate (coordinator, Nov 2: Medium on that condition): closed by lane-change-path’s B-22 follow-up PR (after the journal-versions PR and the move-probe PR, coordinator, Nov 3) (resolved and proxy watches; generic-netlink WireGuard endpoints)—
B-23MediumOpen—
B-24MediumOpen, temporary: closed by lane-change-path’s move-probe PR (fexit on __dev_change_net_namespace: the move becomes an Interface claim for the mover, cleared when the device goes), after the journal-versions PR and before the B-22 follow-up (coordinator, Nov 3)—
B-25MediumOpen, temporary: closed by lane-nix’s acquirable-capabilities PR (with before/after tool counts and boot time)—
B-26MediumOpen, temporary: closed by the initrd-watch work (lane-change-path and lane-nix: pinned bpffs links and maps, layout and build-hash check, root 0700 pins, ring sized from a measured event count)—
B-3MediumClosed (725f290)I8, I19
B-4MediumOpen (v4.5 §17a)I14, I21, I22
B-5MediumClosed (02ba283)I21, I22
B-6MediumClosed (725f290)I21
B-7MediumClosed (89eb2b4)I8
B-8MediumOpen (v4.5 I18, §12c)I18
B-9MediumOpen (v4.5 §12d)I20
X-1AcceptedAcceptedall
X-10AcceptedAcceptedI22, I24
X-11AcceptedAcceptedI22
X-12AcceptedAcceptedI23
X-13AcceptedAcceptedI15, I21
X-2AcceptedAcceptedall
X-3AcceptedAcceptedI2, I20
X-4AcceptedAcceptedall
X-5AcceptedAcceptedI4
X-6AcceptedAcceptedall
X-7AcceptedAcceptedall
X-8AcceptedAcceptedI8
X-9AcceptedAccepted—

B-1 · High

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An agent started by a user runs under that uid and holds its capabilities, including Administrator for a planarian-admin member

BreaksStatusMitigationCloses in
Correct (authority)Closed (544de86)No longer a defect: by owner decision (2026-10-30) an agent holds its user’s capabilities, as X-11 records. planarian agent run (DynamicUser=, unit planarian-agent-&lt;name&gt;, identified by cgroup through the peer’s pidfd) journals who started it, so the agent acts with exactly that user’s permissions and every request is attributed to it; one with no journaled start holds nothing. Gym: agent-run-own-principal, agent-has-its-users-permissions (fake and VM)A launcher and docs; B required; C refused by agent-host profile

The guarantees it narrows

I22.

B-10 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The boot report checks units, not processes

BreaksStatusMitigationCloses in
Always invokedOpen (v4.5 §10)Early processes outside the closureC

The guarantees it narrows

I20.

B-11 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Under Standard, any non-Security stop by signal or unissued systemctl stop is accepted as an inferred override until boot

BreaksStatusMitigationCloses in
Correct (repair)Closed (c84075d)inferStops = false for backup, sync and desktop units; inferred overrides listed until acknowledged (planarian ack). Gym: residue-infer-stops-false, residue-inferred-listed-until-ack (fake and VM)A

The guarantees it narrows

I13.

B-12 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The core hash is written by the same build it checks and covers only planariad

BreaksStatusMitigationCloses in
TamperproofClosed (cc86ca5)Every Planarian binary and posture script is in the core-hash record, each checked by planarian-recovery; signatures in E. Gym: posture-integrity-core-hash, posture-integrity-core-script (VM)A coverage; E signature

The guarantees it narrows

I2.

B-13 · Low

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The gym-hooks package accepts fault injection and stale-digest proposals over the socket

BreaksStatusMitigationCloses in
Always invokedClosed (cc86ca5)Module assertion; a test build started without the gym’s permission exits as an Integrity failure. Gym: test-build-is-integrity (VM); checks.module-evalA

The guarantees it narrows

I14.

B-14 · Low

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Specifications and proof tools (Verus, Z3, rustc) are trusted but uncounted; a wrong spec is certified by its proof

BreaksStatusMitigationCloses in
Correct (proof)Closed (e49c37a)Counted as proof-validity trust (V7–V11); a gym entry per proved invariant written from the English (en-i5 … en-i23), and planarian-gym verification fails if a proved invariant has none. A wrong specification is still certified by its proof; what changed is that the English entries can now catch itA

The guarantees it narrows

Every invariant.

B-15 · Low

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Many dynamic-uid services can fill the global connection cap

BreaksStatusMitigationCloses in
Always invoked (availability)Closed (db5b0f4)16 of the 64 slots reserved for administrators and callers in a login sessionA

The guarantees it narrows

None: a process or availability row.

B-16 · Low

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The external source’s replay protection lives in a root-writable cache

BreaksStatusMitigationCloses in
Correct (I14 input)Closed (db5b0f4)Last accepted sequence number and digest in the journal (and so its checkpoint), held by a verified ledger ruleA (before any producer)

The guarantees it narrows

I14.

B-17 · Low

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

guarantees.md says Kani runs by hand; CI runs it

BreaksStatusMitigationCloses in
—Closed (8bf52c9)Corrected in guarantees.md and verification.mdA

The guarantees it narrows

None: a process or availability row.

B-18 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The model service escapes its sandbox before I9 is proved, and becomes a process with its uid’s capabilities (it parses untrusted text and drives a GPU)

BreaksStatusMitigationCloses in
Tamperproof (I9)Open (v4.5 §20, I9)Normative sandbox: dynamic uid, no capabilities, strict read-only filesystem, Unix sockets only, syscall allowlist, render node only, proposal socket as the only write path; gym case attempting every other write pathB sandbox and case; E proof

The guarantees it narrows

I9.

B-19 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A package referenced only by a store-path string (an ExecStart= naming /nix/store/… directly, a script’s interpolated path) has no meta to read at evaluation, so a fix to it does not move the security floor

BreaksStatusMitigationCloses in
Correct (I16)OpenResidue of B-2: the evaluated inspected set covers every package value; the CVE feed matching the built closure covers stringsB

The guarantees it narrows

I16.

B-2 · High

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The security floor is derived from environment.systemPackages only; fixes in service packages, the kernel or firmware do not move it

BreaksStatusMitigationCloses in
Correct (I16)Closed (d2e173b)Derived from every package value the evaluated configuration holds (system and unit packages, enabled services’ package options, unit path, kernel, firmware) and their runtime inputs; the residue is B-19. Gym: floor-service-package-fix (VM)A closure; B feed

The guarantees it narrows

I16.

B-20 · High

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A second planarian agent run under a running agent’s name rebinds that agent: authority was looked up by name, so an administrator re-running agent run helper made alice’s running agent an administrator’s

BreaksStatusMitigationCloses in
Correct (authority)Closed (8231a07)I24: authority bound to the unit instance (InvocationID, read from the peer’s cgroup xattr) a start journaled after it succeeded; agent run refuses a name in use, naming its owner. Gym: agent-name-collision (fake and VM)A

The guarantees it narrows

I24.

B-21 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

§12d, as amended on #34, defines network-security units by a classifier: network tools (from their systemd configuration) that the kernel attributes as protecting traffic (netwatch). This PR ships the interim rule only: a unit tagged both Security and External, or the firewall by name. Until the classifier lands, an untagged VPN, WireGuard or tailscaled unit holds nothing when it fails, and vm-netsec’s classifier subtests are expected to fail here

BreaksStatusMitigationCloses in
Correct (§12d not yet met)Closed (lane-nix/network-tool #38, with netwatch #42)The classifier (§12d “Which units”): every network tool, computed from its systemd configuration, carries the OnFailure= hold, and the hold’s condition reads its kernel-attributed footprint; no name list. module-eval asserts each branchA

The guarantees it narrows

None: a process or availability row.

B-22 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

netwatch (#42) does not yet observe every steering kind §12d names. (a) DNS servers on a tool’s interface (set through systemd-resolved over D-Bus, or by writing /etc/resolv.conf) and the system proxy (environment or settings) never pass the netlink probes: ChangeKind::Dns and Proxy are produced only by the gym’s simulator. A tool whose only steering is DNS or the proxy (a DNS-over-VPN client, a DNS leak guard, a Tor or SOCKS front end) reads protecting no, so its failure holds nothing and queries or traffic go out directly: fails open. (b) WireGuard peer endpoints are configured over generic netlink, which netwatch does not parse (genl → nothing), so no footprint has endpoint lines. The tunnel’s interface and routes are seen, so its failure holds (fails closed), but the hold does not pass its peer: it cannot re-handshake, its Readiness probe cannot pass, and the hold lifts only by an administrator’s planarian hold release

BreaksStatusMitigationCloses in
Correct (§12d not yet met: inputs to the hold)Open, temporary, and closes before the Phase A gate (coordinator, Nov 2: Medium on that condition): closed by lane-change-path’s B-22 follow-up PR (after the journal-versions PR and the move-probe PR, coordinator, Nov 3) (resolved and proxy watches; generic-netlink WireGuard endpoints)(a) Declare a DNS- or proxy-only tool’s steering explicitly until then (a manifest holdInterfaces/holdAllow entry, or tag the unit so the hold treats it as protecting); (b) declare each VPN peer in the manifest’s holdAllow, so the hold passes it and repair can succeedA

The guarantees it narrows

None: a process or availability row.

B-23 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A network tool that exists only as a raw unit file (shipped in a package through systemd.packages and never declared in systemd.services) is not classified: the build-time rule reads the module system’s configuration, and reading unit files at evaluation would be import from derivation. Its failure holds nothing, and it is not ordered after netwatch. User units are not this gap: a user manager grants no capabilities, so a user unit steers host traffic only by asking a system service (whose footprint carries the change) or through a setuid or file-capability binary (the parked gap in #38). Related, and failing closed rather than open: with networking in the systemd initrd (boot.initrd.network, or networkd there), links and routes still standing at switch-root were made before netwatch existed and read as pre-attach, so every network tool’s failure holds until they are gone

BreaksStatusMitigationCloses in
Correct (§12d overstated)OpenWorkaround: declare the unit in systemd.services (even only systemd.services.&lt;name&gt; = { }; beside the package; NixOS writes it as a drop-in over the package’s file), and it is classified, held and ordered. Fix: classify raw units at build time from the unit tree itself (the drop-in package reading the files it ships beside), not at evaluationB

The guarantees it narrows

None: a process or availability row.

B-24 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An interface moved into the host netns from another (ip link set <dev> netns 1 run inside a container or ip netns) is seen only as the mover’s own request, in the mover’s netns, which names the device by its index there; netwatch cannot tell which host device arrived. It charges the move to the mover’s unit as unattributed (protecting yes), with no object to clear it by, so the mover stays protecting until reboot, even after the device is deleted. Fail closed: its failure holds traffic it may no longer steer

BreaksStatusMitigationCloses in
Available (§12d: a hold for a tool that no longer protects)Open, temporary: closed by lane-change-path’s move-probe PR (fexit on __dev_change_net_namespace: the move becomes an Interface claim for the mover, cleared when the device goes), after the journal-versions PR and before the B-22 follow-up (coordinator, Nov 3)planarian hold release &lt;unit&gt; lifts a hold the mover no longer warrants; or restart the machineA

The guarantees it narrows

None: a process or availability row.

B-25 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

§12d (Nov 3) counts a unit that could gain CAP_NET_ADMIN or CAP_NET_RAW as a network tool: its bounding set allows it and NoNewPrivileges= is unset, so a setuid or file-capability binary such as ping can grant it. The merged classifier (nix/module.nix, isNetworkToolUnchecked) reads the bounding set only for root units and never reads NoNewPrivileges=. A non-root unit that can gain the caps this way is not a tool: it is not ordered after netwatch, and its failure holds nothing even while it steers traffic: fails open

BreaksStatusMitigationCloses in
Correct (§12d not yet met)Open, temporary: closed by lane-nix’s acquirable-capabilities PR (with before/after tool counts and boot time)None until then; declare the unit with NoNewPrivileges=true or a bounding set without the network caps (tagging no longer makes a unit a tool, since #38)A

The guarantees it narrows

None: a process or availability row.

B-26 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

§12d (Nov 3) runs the watch in the systemd initrd, so no network tool acts unwatched in either stage. Main still exempts initrd survivors: the units in nix/netwatch-unordered.nix (started by the systemd initrd and kept across switch-root) are not ordered after the watch, so what they change before stage-2 netwatch attaches is attributed to nobody. Steering from that window that still stands at attach is read as pre-attach <object> (netwatch’s attach rule; N6) and holds every network tool’s failure until the object is gone: fails closed (over-holding). It fails open only for steering the attach snapshot cannot read, DNS servers set through resolved and the system proxy, which is B-22’s gap

BreaksStatusMitigationCloses in
Available (§12d not yet met); Correct only through B-22Open, temporary: closed by the initrd-watch work (lane-change-path and lane-nix: pinned bpffs links and maps, layout and build-hash check, root 0700 pins, ring sized from a measured event count)planarian hold release &lt;unit&gt; lifts a hold a pre-attach object no longer warrants; with a scripted initrd there are no such unitsA

The guarantees it narrows

None: a process or availability row.

B-3 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Request::Approve { tty } is set by the client; any root process can claim a terminal

BreaksStatusMitigationCloses in
Correct (approval)Closed (725f290)Daemon reads the peer’s controlling terminal from its pidfd; field removed. Gym: approve-needs-real-terminal (fake and VM)A; broker in C

The guarantees it narrows

I8, I19.

B-4 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

decide is proved, but its inputs (DerivedFacts, Authorization) are computed by unproven code

BreaksStatusMitigationCloses in
Correct (inputs)Open (v4.5 §17a)Internal-unproven count reported separately; fail-closed defaults; derive proved in CA accounting; C proof

The guarantees it narrows

I14, I21, I22.

B-5 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Untagged security units (fail2ban, AppArmor, USBGuard, a VPN) are Routine everywhere

BreaksStatusMitigationCloses in
Correct (classification)Closed (02ba283)Security by default for the named units and option trees; an undeclared or untagged system unit is External (fail closed); the named list is checked by checks.module-evalA

The guarantees it narrows

I21, I22.

B-6 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Session resolved from a bare pid; same-uid pid reuse attributes the wrong session (I21)

BreaksStatusMitigationCloses in
Correct (identity)Closed (725f290)SO_PEERPIDFD and pidfd-based lookups; a peer gone before it is resolved is its uid alone, with no session or terminal. Pid reuse itself cannot be staged reproducibly; the guarantee rests on P10. Gym: identity-peer-exited (VM)A

The guarantees it narrows

I21.

B-7 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Token expiry, leases and cooldowns use wall-clock time; moving it back extends approvals; unauthenticated NTP lets a network attacker do it

BreaksStatusMitigationCloses in
Correct (I8 expiry)Closed (89eb2b4)CLOCK_BOOTTIME for tokens, leases and deadlines; per-process anchored wall clock for the rest; NTS recommendedA

The guarantees it narrows

I8.

B-8 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The journal hash chain is unkeyed; root can rewrite it consistently

BreaksStatusMitigationCloses in
TamperproofOpen (v4.5 I18, §12c)I18 qualified; chain head in the signed health report for off-machine witnessing; TPM anchorA wording; C witness; E anchor

The guarantees it narrows

I18.

B-9 · Medium

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Posture alarms and notification are local; root can clear them and a stopped planariad can go unnoticed

BreaksStatusMitigationCloses in
TamperproofOpen (v4.5 §12d)Dead-man signal from the signed health report to an off-machine receiverC

The guarantees it narrows

I20.

X-1 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Root edits the store, /boot, the binaries, the journal or the posture state

BreaksStatusMitigationCloses in
AllAcceptedDetection only: boot report, hash chain, core hashE

The guarantees it narrows

Every invariant.

X-10 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A program a user runs under their own uid, outside planarian agent run (an agent started by hand from a shell)

BreaksStatusMitigationCloses in
Correct (authority)AcceptedIt is that user, with that user’s capabilities, as anything they run is, and unattributed: Planarian cannot tell a program from its user. The README says to use planarian agent run, which gives the same permissions with attribution; C’s agent-host profile refuses agents outside itC

The guarantees it narrows

I22, I24.

X-11 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An agent holds its user’s capabilities, including Administrator when an administrator starts it (owner decision, 2026-10-30)

BreaksStatusMitigationCloses in
Correct (authority)AcceptedBy design, not a defect: agents are not treated differently from their users in what they may do. Every request is attributed to the agent in the executor log; who started it is in the hash-chained journal; it has no login session or terminal, so it cannot approve an AskFirst change through the daemon’s terminal check (unless it allocates its own pty, the known weakness of §16), and its own AskFirst proposals are its user’s, which that user cannot approve (I19)—

The guarantees it narrows

I22.

X-12 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A writer that does not take the daemon’s lock on the override directory (an editor, a script) replaces planarian/overrides.nix between the daemon’s comparison and its rename

BreaksStatusMitigationCloses in
Correct (I23)AcceptedAn edit made before the comparison is caught and never overwritten (en-i23-third-party-override-kept); a writer that takes the daemon’s exclusive flock on planarian/ cannot land between them: the compare and the rename are one step under it (§9 step 4; the_override_swap_waits_for_the_directory_lock, and on the VM en-i23-third-party-override-kept)C (writer boundary)

The guarantees it narrows

I23.

X-13 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The network hold (§12d Network security fails closed): planariad applies and lifts planarian-hold@<unit> with no Prepared record, no lease and no op tag, mirroring systemd’s OnFailure=; the hold drops connectivity without asking

BreaksStatusMitigationCloses in
Correct (I15, I21)AcceptedA failure posture response, the same class as §12d’s other responses, not a transition (coordinator, Nov 2). Safe because applying only ever restricts traffic; lifting follows only a unit observed Running with every Readiness probe passing, or an attested release; both calls are idempotent. Auditable: HoldApplied { unit, by } and HoldLifted { unit, reason } are journaled before each call (hash-chained), and the journal’s holds are the recovery source of truth, reconciled with the host’s after a restart, failing closed. The decision to unblock is never the bypass: planarian hold release is an attested Override on the hold unit through the executor, administrator authority or AskFirst with an Administrator approver. Gym: core tests a_held_unit_crash_holds_restarts_at_once_and_lifts, an_unattributed_stop_is_held_by_the_daemon_and_an_attested_one_is_not, a_failed_repair_keeps_the_hold_and_notifies_across_a_restart, hold_release_needs_an_administrator; VM vm-netsecA

The guarantees it narrows

I15, I21.

X-2 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A nixos-rebuild outside Planarian

BreaksStatusMitigationCloses in
Always invokedAcceptedObserved as an unconfirmed base, never trusted; wrapped by PlanarianOSC

The guarantees it narrows

Every invariant.

X-3 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Firmware, bootloader and initrd run before planarian-ready.target

BreaksStatusMitigationCloses in
Always invokedAcceptedBoot-chain locking and Secure Boot (C); measured boot (E); nothing below firmwareC, E

The guarantees it narrows

I2, I20.

X-4 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Physical access

BreaksStatusMitigationCloses in
AllAcceptedDisk sealingE

The guarantees it narrows

Every invariant.

X-5 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A malicious unit that works correctly and passes its probes

BreaksStatusMitigationCloses in
CorrectAcceptedNothing; the malicious-declaration entries score whether its origin is reported—

The guarantees it narrows

I4.

X-6 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The supply chain: nixpkgs content, cache signatures, input-addressed store paths

BreaksStatusMitigationCloses in
CorrectAcceptedNix’s boundary, not Planarian’s—

The guarantees it narrows

Every invariant.

X-7 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Backends report success falsely; systemd or the kernel misbehave (P1, P6, P8)

BreaksStatusMitigationCloses in
CorrectAcceptedRe-observation and probes after every call; replaced in D and ED, E

The guarantees it narrows

Every invariant.

X-8 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The approver: fatigue, social engineering, persuasion by a model

BreaksStatusMitigationCloses in
CorrectAcceptedAskFirst kept rare (per-week ratchet); the change shown in full—

The guarantees it narrows

I8.

X-9 · Accepted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Withdrawn 2026-10-29. The model’s judgement is not a vector: the model is treated as an adversary, so a bad proposal is one any attacker could make and is a policy, catalog or probe finding under its own name. Containment is B-18; persuading an approver is X-8

BreaksStatusMitigationCloses in
—Accepted——

The guarantees it narrows

None: a process or availability row.

Gym entries

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

EntryHarnessInvariantsLatest verdict
agent-has-its-users-permissionsfake, vmfake: PASS; VM: not run in this build
agent-name-collisionfake, vmI24fake: PASS; VM: not run in this build
agent-run-own-principalfake, vmfake: PASS; VM: not run in this build
approval-dies-with-daemonfake, vmfake: PASS; VM: not run in this build
approval-token-expiredfakeI8fake: PASS
approval-token-invalidatedfakeI8fake: PASS
approve-at-terminal-runsfake, vmfake: PASS; VM: not run in this build
approve-needs-real-terminalfake, vmI14fake: PASS; VM: not run in this build
authz-forged-actor-overwrittenfake, vmI14fake: PASS; VM: not run in this build
authz-nonadmin-stops-network-other-sessionfakefake: PASS
authz-unprivileged-override-securityfake, vmfake: PASS; VM: not run in this build
authz-unprivileged-stop-unauthorizedfake, vmfake: PASS; VM: not run in this build
authz-user-overrides-firewallfakeI19fake: PASS
awaiting-boot-reminds-dailyfakeI12fake: PASS
boot-only-admitted-not-confirmedfakeI6fake: PASS
boot-only-kernel-boot-failsfakeI1fake: PASS
boot-only-kernel-confirmedfakefake: PASS
boot-only-repair-during-confirmationfakeI6fake: PASS
built-parking-expiresfakeI12fake: PASS
candidate-nar-mismatch-refusedfakefake: PASS
catalog-change-carries-unbuilt-owner-editfakeI14fake: PASS
checkpoint-bad-crcfakefake: PASS
cond-device-unpluggedvmVM: not run in this build
core-target-rejectedfakefake: PASS
crash-activating-badfake, vmI6, I12, I15fake: PASS; VM: not run in this build
crash-activating-goodfake, vmI6, I12, I15fake: PASS; VM: not run in this build
crash-checkingfake, vmfake: PASS; VM: not run in this build
crash-daemon-restart-reconcilesfake, vmfake: PASS; VM: not run in this build
crash-job-outlives-drainfake, vmfake: PASS; VM: not run in this build
crash-jobs-tagged-unknownfakefake: PASS
crash-promotedfake, vmfake: PASS; VM: not run in this build
crash-publishedfake, vmfake: PASS; VM: not run in this build
crash-publishing-after-publishfakefake: PASS
crash-publishing-before-publishfakefake: PASS
crash-revertingfake, vmfake: PASS; VM: not run in this build
crash-runtime-rollbackfake, vmfake: PASS; VM: not run in this build
crash-simultaneous-takeoverfake, vmI15fake: PASS; VM: not run in this build
crash-slow-worker-overlapfake, vmI6, I15fake: PASS; VM: not run in this build
crash-stop-and-suppressfake, vmfake: PASS; VM: not run in this build
crash-withheld-job-replyfakeI6, I15fake: PASS
degraded-unit-loses-a-probefakefake: PASS
dep-ordering-only-stoppedfake, vmfake: PASS; VM: not run in this build
dep-required-stoppedfake, vmfake: PASS; VM: not run in this build
dep-required-stopped-guardedfake, vmfake: PASS; VM: not run in this build
dep-wants-optional-failedfake, vmfake: PASS; VM: not run in this build
desktop-auth-ok-compositor-failsvmVM: not run in this build
desktop-boot-fails-rollbackvmI1VM: not run in this build
desktop-second-user-deadlinevmVM: not run in this build
disruption-connectivity-alone-seated-runsfakefake: PASS
disruption-each-kind-keeps-its-floorfakefake: PASS
drv-bind-fails-restores-previousfake, vmfake: PASS; VM: not run in this build
drv-unbound-audiofake, vmfake: PASS; VM: not run in this build
drv-unbound-nic-asksvmVM: not run in this build
dry-activate-exit-nonzerofakefake: PASS
dry-activate-timeoutfakefake: PASS
dry-activate-truncatedfakefake: PASS
en-i10-a-canonical-probe-cannot-be-removedfakeI10fake: PASS
en-i11-session-carries-its-sensitivityfakeI11fake: PASS
en-i12-nothing-left-openfakeI12fake: PASS
en-i13-overridden-unit-is-not-repairedfake, vmI13fake: PASS; VM: not run in this build
en-i14-forged-fields-ignoredfake, vmI14fake: PASS; VM: not run in this build
en-i15-old-worker-cannot-finishfake, vmI15fake: PASS; VM: not run in this build
en-i16-boot-fallback-respects-floorfakeI16fake: PASS
en-i17-root-administers-without-the-groupfakeI17fake: PASS
en-i18-deleted-record-detectedfake, vmI18fake: PASS; VM: not run in this build
en-i19-no-self-approvalfake, vmI19fake: PASS; VM: not run in this build
en-i21-irreversible-asks-even-for-rootfakeI21fake: PASS
en-i22-user-cannot-do-admin-thingsfake, vmI22fake: PASS; VM: not run in this build
en-i23-third-party-override-keptfake, vmI23fake: PASS; VM: not run in this build
en-i5-reject-survives-every-settingfakeI5fake: PASS
en-i6-repair-waits-for-activationfake, vmI6fake: PASS; VM: not run in this build
en-i8-token-bound-to-candidatefake, vmI8fake: PASS; VM: not run in this build
fake-assumptions-conformancefakeI6, I12, I15fake: PASS
flap-30sfake, vmfake: PASS; VM: not run in this build
flap-regression-escalatesfakefake: PASS
floor-manual-rollback-needs-adminfakeI16, I19fake: PASS
floor-moves-via-boot-onlyfakeI25fake: PASS
floor-moves-via-watchdogfakeI16, I25fake: PASS
floor-revert-blockedfakeI1, I16fake: PASS
floor-service-package-fixvmI16VM: not run in this build
gc-during-approvalfake, vmI3fake: PASS; VM: not run in this build
gc-retain-all-generationsfakeI21fake: PASS
gen-bad-switchfake, vmfake: PASS; VM: not run in this build
gen-disable-reenable-canonicalfake, vmI10fake: PASS; VM: not run in this build
gen-override-only-changefake, vmfake: PASS; VM: not run in this build
identity-peer-exitedvmI21VM: not run in this build
intent-cli-stop-rewrittenfake, vmfake: PASS; VM: not run in this build
intent-start-again-repairsfake, vmfake: PASS; VM: not run in this build
intent-user-stops-servicefake, vmI13fake: PASS; VM: not run in this build
journal-compaction-preserves-statefake, vmfake: PASS; VM: not run in this build
journal-record-editedfake, vmI18fake: PASS; VM: not run in this build
journal-torn-writefakefake: PASS
login-greeter-idlevmVM: not run in this build
malicious-decl-catalog-enablefake, vmfake: PASS; VM: not run in this build
malicious-decl-input-updatefake, vmfake: PASS; VM: not run in this build
malicious-decl-owner-rebuildfake, vmfake: PASS; VM: not run in this build
manifest-weaken-out-of-bandvmI10VM: not run in this build
manifest-weaken-via-proposalfake, vmI10fake: PASS; VM: not run in this build
net-external-dependency-downfake, vmfake: PASS; VM: not run in this build
net-gateway-downfake, vmfake: PASS; VM: not run in this build
net-upstream-downfake, vmfake: PASS; VM: not run in this build
override-over-attested-refusedfakefake: PASS
override-stopped-equals-stopfake, vmI21fake: PASS; VM: not run in this build
owner-rebuild-breaks-servicefake, vmfake: PASS; VM: not run in this build
policy-daemon-session-drop-rebindfake, vmfake: PASS; VM: not run in this build
policy-daemon-wifi-runtimefake, vmfake: PASS; VM: not run in this build
policy-refinement-violationfakefake: PASS
policy-stale-digestfakefake: PASS
posture-absent-maskedvmI20VM: not run in this build
posture-crashed-budgetvmI20VM: not run in this build
posture-integrity-core-hashvmI2, I20VM: not run in this build
posture-integrity-core-scriptvmI2, I20VM: not run in this build
posture-integrity-journal-chainvmI18, I20VM: not run in this build
posture-integrity-undeclared-early-unitvmI20VM: not run in this build
posture-stops-launched-agentsvmI20VM: not run in this build
probe-delayed-healthyfake, vmfake: PASS; VM: not run in this build
probe-diagnostic-stale-healthyfake, vmI4fake: PASS; VM: not run in this build
probe-failed-beats-stalefake, vmfake: PASS; VM: not run in this build
probe-http-503-degradedfake, vmfake: PASS; VM: not run in this build
probe-trickling-endpointvmVM: not run in this build
quiet-reference-workloadbench
quiet-repair-not-deferredfakefake: PASS
quiet-staged-update-foregroundfakeI6fake: PASS
release-refused-leaves-incidentfake, vmfake: PASS; VM: not run in this build
reprepared-keeps-callerfakefake: PASS
residue-first-kill-teaches-stopfakefake: PASS
residue-infer-stops-falsefake, vmfake: PASS; VM: not run in this build
residue-inferred-listed-until-ackfake, vmfake: PASS; VM: not run in this build
residue-kill9-securityfake, vmI22fake: PASS; VM: not run in this build
residue-kill9-servicefake, vmfake: PASS; VM: not run in this build
residue-kill9-undofakefake: PASS
residue-oom-killfake, vmfake: PASS; VM: not run in this build
residue-stop-security-unitfake, vmI22fake: PASS; VM: not run in this build
revert-failsfake, vmfake: PASS; VM: not run in this build
revert-fails-interlock-survives-compactionfake, vmfake: PASS; VM: not run in this build
revert-to-older-sets-no-floorfakeI25fake: PASS
rollback-repair-foreground-dependencyfakefake: PASS
rollback-repair-foreground-unrelatedfakefake: PASS
rollback-repair-session-loss-asksfakefake: PASS
session-logout-one-of-twofakefake: PASS
session-second-compositor-failsfakefake: PASS
ssh-session-expects-no-desktopfakefake: PASS
stale-owner-edit-during-buildfake, vmI23fake: PASS; VM: not run in this build
stale-untracked-file-ignoredvmVM: not run in this build
storage-boot-full-publishfake, vmfake: PASS; VM: not run in this build
storage-budget-unsatisfiablefake, vmI3fake: PASS; VM: not run in this build
storage-enospc-during-buildfakefake: PASS
storage-journal-btrfs-reusevmVM: not run in this build
storage-journal-fs-fullvmVM: not run in this build
storage-nix-full-gcfake, vmI3fake: PASS; VM: not run in this build
suspend-resumefakefake: PASS
svc-killedfake, vmfake: PASS; VM: not run in this build
svc-killed-defer-to-systemdvmVM: not run in this build
svc-killed-user-scopevmVM: not run in this build
svc-misconfig-listen-addressfake, vmfake: PASS; VM: not run in this build
svc-misconfig-state-filevmVM: not run in this build
switch-facts-from-dry-activationfakefake: PASS
test-build-is-integrityvmVM: not run in this build
token-wall-clock-moved-backfakeI8fake: PASS
unauthorized-change-does-no-workfakefake: PASS
user-unit-after-logoutvmVM: not run in this build

agent-has-its-users-permissions

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice (uid 1000, not an administrator) starts an agent with planarian agent run helper. It has alice’s permissions and no more: its stop of gym-web, a system service alice may not stop, is refused Unauthorized; its stop of alice’s own pipewire runs. Both are attributed to agent:helper.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: service:gym-web StopAndSuppress, service@1000:pipewire Committed StopAndSuppress; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

agent-name-collision

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice (uid 1000) starts an agent named helper. An administrator then runs planarian agent run helper too: refused, naming alice as its owner. alice’s agent stays alice’s: its stop of gym-web, a system unit she may not stop, is refused. Authority is bound to the instance a start journaled, never the name (I24).

HarnessScoringExpectsLatest verdict
fake, vmmust-passreports: AgentNameInUse; txn: service:gym-web StopAndSuppress; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

I24

agent-run-own-principal

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator (uid 1001, planarian-admin) starts an agent with planarian agent run coder. The agent runs under a dynamic uid in its own unit, and acts with the permissions of the user who started it (§11, owner decision 2026-10-30): its stop of gym-web runs, as the administrator’s would, and the executor log attributes it to agent:coder, acting for uid 1001.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: service:gym-web Committed StopAndSuppress; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

approval-dies-with-daemon

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator asks to disable sshd (AskFirst) and the approval is issued. The daemon restarts before anyone approves. The old approval is void: presenting it is refused, the change is asked for again, and nothing runs until the new request is approved.

HarnessScoringExpectsLatest verdict
fake, vmmust-passreports: TokenReplayRefused, NeedsApproval; txn: pending ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

approval-token-expired

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A parked AskFirst change gets no approval for 24 h. It ends Rejected, its GC root is released, and any waiting incident reopens.

HarnessScoringExpectsLatest verdict
fakemust-passgc_root_released: true; txn: ApplyChangefake: PASS

What it exercises

I8

approval-token-invalidated

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A token is issued for a parked AskFirst change, then the policy version changes. Re-validation invalidates the token and re-requests approval, and replaying the old token is refused.

HarnessScoringExpectsLatest verdict
fakemust-passreports: ApprovalInvalidated, TokenReplayRefused; txn: ApplyChangefake: PASS

What it exercises

I8

approve-at-terminal-runs

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator who is not root asks to disable sshd (Security: AskFirst). Root approves it from a real terminal, which the daemon reads from the peer’s pidfd: the approval is taken and the change commits.

HarnessScoringExpectsLatest verdict
fake, vmmust-passapproval_recorded_by: root; txn: Committed ApplyChangefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

approve-needs-real-terminal

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Root approves a pending AskFirst change from a process with no controlling terminal (a new session, as a service would be). The daemon reads the terminal from the peer’s pidfd, finds none, and refuses: the change is still waiting at the end. The request carries no terminal field to claim.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: pending ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

I14

authz-forged-actor-overwritten

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice submits a SetOverride for her own pipewire with by = Daemon. SO_PEERCRED overwrites the actor to user:1000; the override is allowed and attributed to her.

HarnessScoringExpectsLatest verdict
fake, vmmust-passoverrides: service@1000:pipewirefake: PASS; VM: not run in this build

What it exercises

I14

authz-nonadmin-stops-network-other-session

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

bob, at the seat, stops the gym network interface (made user-controllable for this case, G11) while alice has a session open. The derived facts say it reaches another uid’s session; it asks, and alice’s session is notified.

HarnessScoringExpectsLatest verdict
fakemust-passnotified_sessions: s1; txn: pending StopAndSuppressfake: PASS

What it exercises

No invariant by name.

authz-unprivileged-override-security

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice tries to override sshd, a system Security unit. It is held as AskFirst for an administrator; sshd stays up meanwhile.

HarnessScoringExpectsLatest verdict
fake, vmmust-passoverrides: ; txn: pending SetOverride; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

authz-unprivileged-stop-unauthorized

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice tries to stop gym-db, a system unit that is neither Security-sensitive nor user-controllable: Reject(Unauthorized), whatever max_tier says.

HarnessScoringExpectsLatest verdict
fake, vmmust-passreports: Unauthorized; txn: StopAndSuppress; units: service:gym-db Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

authz-user-overrides-firewall

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

From her session, alice overrides the firewall (sshd’s stand-in in the fake: a Security unit) to stopped. Held for an administrator; it stays up. alice cannot approve her own request (I19); root at a terminal approves, and the override holds, attested and attributed to alice.

HarnessScoringExpectsLatest verdict
fakemust-passinvariants: I19; overrides: service:sshd; txn: Committed SetOverridefake: PASS

What it exercises

I19

awaiting-boot-reminds-daily

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A kernel parameter change takes the boot-only path and nobody restarts for two and a half days. It stays AwaitingBoot (no limit applies, nothing is cancelled), the ‘restart to finish’ notice is repeated once a day, and at the eventual restart it is confirmed and commits.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: candidate; notices_count: count 2, match is still waiting for a restart; reports: RestartToFinish; txn: Committed ApplyChangefake: PASS

What it exercises

I12

boot-only-admitted-not-confirmed

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A boot-only kernel change boots and is admitted (Promoted: the candidate is the default, its override promoted). sshd then crashes; repairs run while the admitted change is unconfirmed, so sshd is restarted at once, but a canonical unit that needed a repair counts against the change: at the 10-minute checkpoint it is withdrawn, never committed: Reverted, the publishing generation is the default again, the base override restored, and a notice names sshd and says a restart completes it.

HarnessScoringExpectsLatest verdict
fakemust-passboot_default: base; notices_count: count 1, match not confirmed (service:sshd needed a repair before the 10-minute checkpoint); txn: Reverted ApplyChange, service:sshd Committed RestartUnit; units: service:sshd Runningfake: PASS

What it exercises

I6

boot-only-kernel-boot-fails

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The same change, but the one-shot boot fails; the boot loader falls back to the default (the base), and the transaction is Reverted.

HarnessScoringExpectsLatest verdict
fakemust-passboot_default: base; generation: base; txn: ApplyChangefake: PASS

What it exercises

I1

boot-only-kernel-confirmed

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A kernel parameter change takes the boot-only path: published as a one-shot next-boot entry, AwaitingBoot, a ‘restart to finish’ notice, the base still the default. At the next boot it is confirmed, becomes the default, and commits.

HarnessScoringExpectsLatest verdict
fakemust-passagrees: true; boot_default: candidate; generation: candidate; known_good: candidate; live_tested: never; reports: RestartToFinish; txn: ApplyChangefake: PASS

What it exercises

No invariant by name.

boot-only-repair-during-confirmation

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A boot-only kernel change boots and is admitted. Two minutes later gym-web (not canonical) crashes: it is repaired at once, without waiting for the 10-minute checkpoint, and the change is still confirmed and committed there.

HarnessScoringExpectsLatest verdict
fakemust-passagrees: true; boot_default: candidate; generation: candidate; txn: Committed ApplyChange, service:gym-web Committed RestartUnit; units: service:gym-web Runningfake: PASS

What it exercises

I6

built-parking-expires

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice changes her font scale while a game holds Foreground in her session, and the game never lets go. After a day in Built the change is Cancelled by the daemon, with one notice naming it and how to issue it again; it never activates, even once the game ends.

HarnessScoringExpectsLatest verdict
fakemust-passnotices_count: count 1, match waited a day in Built; reports: WaitsForWindow; txn: Cancelled ApplyChangefake: PASS

What it exercises

I12

candidate-nar-mismatch-refused

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A display-scale candidate’s store contents no longer hash to the NAR hash recorded when it was built. It is refused before activation: verification checks the hash, not that the path exists (§9 step 3, C6).

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; override_source: base; txn: Rejected(Other(“candidate failed verification”)) ApplyChangefake: PASS

What it exercises

No invariant by name.

catalog-change-carries-unbuilt-owner-edit

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator has edited the owner’s configuration to enable a service but not rebuilt. alice then asks for a user-controllable change (font scale). The candidate would also bring the new service, which the override does not explain, so the change is Security-sensitive: it waits for approval instead of activating on its own.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; reports: NeedsApprovalfake: PASS

What it exercises

I14

checkpoint-bad-crc

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The journal is compacted, then its checkpoint is damaged while the daemon is down (its CRC no longer matches). The daemon does not start from nothing: it reports an Integrity failure naming the journal.

HarnessScoringExpectsLatest verdict
fakemust-passposture: class Integrity; reports: JournalChainBrokenfake: PASS

What it exercises

No invariant by name.

cond-device-unplugged

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-usb0 is hot-unplugged, so gym-usb-reader (desired only under DeviceConnected) stops, and no incident opens.

HarnessScoringExpectsLatest verdict
vmintentno_autonomous_txn: true; no_incident_on: service:gym-usb-reader, device:gym-usb0VM: not run in this build

What it exercises

No invariant by name.

core-target-rejected

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A candidate whose evaluated unit file for planariad differs from the running one is rejected as CoreTarget, under any approval.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: ApplyChangefake: PASS

What it exercises

No invariant by name.

crash-activating-bad

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is killed during Activating of a bad candidate; the watchdog takes the lease, fences, confirms quiescence, and reverts.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: base; generation: base; invariants: I6, I12, I15; journal_agrees: true; override_source: base; txn: Reverted ApplyChange; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

I6, I12, I15

crash-activating-good

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is killed during Activating of a good candidate. The watchdog sees the candidate active, goes to Checking, the probes pass, and it runs the entire commit path.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: candidate; generation: candidate; invariants: I6, I12, I15; journal_agrees: true; override_source: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build

What it exercises

I6, I12, I15

crash-checking

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is killed during Checking; the watchdog re-runs the probes itself before committing.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: candidate; generation: candidate; journal_agrees: true; override_source: candidate; txn: Committed ApplyChange; watchdog_probed_before_commit: truefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-daemon-restart-reconciles

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

GA demo tail: after the watchdog has reverted (crash-activating-bad), planariad restarts and writes a reconciliation record whose account of what happened matches the journal.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgeneration: base; journal_agrees: true; reconciliation: daemon_action none, explains [“killed-during-Activating”, “watchdog-reverted”, “why:candidate not active”], found Reverted, owner watchdogfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-job-outlives-drain

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An old backend job outlives the 60 s drain. The takeover ends in RecoveryFailed with the interlock set, and no overlapping mutation happens.

HarnessScoringExpectsLatest verdict
fake, vmmust-passinterlock: set; no_overlap: activate_live(candidate), activate_live(revert_to); reports: RecoveryFailed, alert; txn: RecoveryFailed ApplyChangefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-jobs-tagged-unknown

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

jobs_tagged returns None for an unacknowledged OpId (the backend cannot say). Recovery writes RecoveryFailed instead of proceeding.

HarnessScoringExpectsLatest verdict
fakemust-passinterlock: set; txn: RecoveryFailed ApplyChangefake: PASS

What it exercises

No invariant by name.

crash-promoted

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The daemon stays down after Promoted; the watchdog writes Committed.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: candidate; generation: candidate; journal_agrees: true; override_source: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-published

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The daemon stays down after Published; the watchdog promotes the override by hash comparison and commits.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: candidate; generation: candidate; journal_agrees: true; override_source: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-publishing-after-publish

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is killed after publish_boot_once armed the one-shot entry, before its outcome was journaled. On restart the Publishing row asks the boot loader: the entry is armed, so the change is AwaitingBoot. The reboot boots the candidate, it is admitted and committed, and boot selection, roots, overrides and the journal agree.

HarnessScoringExpectsLatest verdict
fakemust-passagrees: true; boot_default: candidate; generation: candidate; txn: Committed ApplyChangefake: PASS

What it exercises

No invariant by name.

crash-publishing-before-publish

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is killed after journaling OpBegin(publish_boot_once) for a boot-only kernel change, before the call. On restart the Publishing row asks the boot loader: nothing is armed, so the change is Rejected(Interrupted) and not prepared again. After a reboot the machine runs the publishing generation, and boot selection, roots, overrides and the journal agree.

HarnessScoringExpectsLatest verdict
fakemust-passagrees: true; boot_default: base; generation: base; txn: Rejected(Interrupted) ApplyChangefake: PASS

What it exercises

No invariant by name.

crash-reverting

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is killed during Reverting of a bad candidate; the watchdog re-runs the idempotent revert steps.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: base; generation: base; journal_agrees: true; override_source: base; txn: Reverted ApplyChange; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-runtime-rollback

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Killed during Checking of a Runtime-tier Rollback; it recovers as a generation operation (serialized, owned, publish_boot, override restored) and completes. (Killed before the activation took effect, the crash table reverts instead, which is also correct.)

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: known_good; generation: known_good; journal_agrees: true; override_source: known_good; txn: Committed Rollbackfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-simultaneous-takeover

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The daemon and the watchdog try to take over at the same moment; exactly one wins the flock and the other backs off.

HarnessScoringExpectsLatest verdict
fake, vmmust-passinvariants: I15; journal_agrees: true; lease_epochs_strictly_increasing: true; owners_per_epoch_max: 1fake: PASS; VM: not run in this build

What it exercises

I15

crash-slow-worker-overlap

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The daemon is killed and immediately restarted while a deliberately slow activation worker survives. The new owner fences it, so activation and revert never overlap and the old owner can never commit.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgeneration: base; invariants: I6, I15; journal_agrees: true; no_overlap: activate_live(candidate), activate_live(revert_to); stale_owner_commit: falsefake: PASS; VM: not run in this build

What it exercises

I6, I15

crash-stop-and-suppress

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Killed during a user’s StopAndSuppress. It recovers as a unit operation: the override (journaled before the stop) holds, the unit is not restarted, and no generation rollback is attempted.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgeneration: base; overrides: service:gym-web; txn: Committed StopAndSuppress, Revert, service:gym-web RestartUnit; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

crash-withheld-job-reply

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The backend accepts an activation job but its reply is withheld, and the owner is killed before the job id is journaled. Recovery must discover the job by its OpId, or fence it, before making any conflicting mutation.

HarnessScoringExpectsLatest verdict
fakemust-passinvariants: I6, I15; journal_agrees: true; no_overlap: activate_live(candidate), activate_live(revert_to); quiescence_includes_tagged_jobs: truefake: PASS

What it exercises

I6, I15

degraded-unit-loses-a-probe

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web is already Degraded: its HTTP check fails (503) while its TCP check passes. A change then breaks gym-web’s listen address, so the TCP check fails too. The check after activation compares each Readiness probe that passed before, not the unit’s status, so Checking fails and the change is reverted.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; txn: Reverted ApplyChangefake: PASS

What it exercises

No invariant by name.

dep-ordering-only-stopped

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-cache (After= of gym-app only) is killed. gym-app stays healthy and is never touched; gym-cache is its own incident.

HarnessScoringExpectsLatest verdict
fake, vmrepairincident: service:gym-cache; may_change: service:gym-cache; no_incident_on: service:gym-app; no_txn_on: service:gym-app; others_unchanged: true; units: service:gym-app Running, service:gym-cache Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

dep-required-stopped

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

root stops gym-db out of band; systemd stops gym-app with it (Requires=). v4.4 (owner decision Q5): the unattributed residue. Under Standard both are taken as intended until the next boot (gym-app linked to gym-db), nothing is restarted, and the notice offers the undo.

HarnessScoringExpectsLatest verdict
fake, vmintentno_autonomous_txn: true; no_txn_on: service:gym-db, service:gym-app; overrides: service:gym-db, service:gym-app; reports: InferredOverride, Undo; units: service:gym-app Overridden, service:gym-db Overriddenfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

dep-required-stopped-guarded

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

As dep-required-stopped, under the Guarded posture, where an unattributed stop is not inferred as intended (§11; Phase A repairs it, without the ask window). Root cause is gym-db: it is repaired first, then gym-app.

HarnessScoringExpectsLatest verdict
fake, vmrepairincident: service:gym-db; may_change: service:gym-db, service:gym-app; others_unchanged: true; overrides: ; reports: OutOfBandStopHint; txn: service:gym-db Committed RestartUnit, service:gym-app RestartUnit; units: ser…fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

dep-wants-optional-failed

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-metrics (Wants= of gym-app) fails persistently while gym-app stays healthy: at most one incident, on gym-metrics, and gym-app is never named as caused by it.

HarnessScoringExpectsLatest verdict
fake, vmmust-passincident: service:gym-metrics; metrics: incidents_max 1; no_incident_on: service:gym-app; no_txn_on: service:gym-app; others_unchanged: true; units: service:gym-app Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

desktop-auth-ok-compositor-fails

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice authenticates at the greeter, but her compositor never reaches graphical-session.target, and an incident opens within 60 s of the session’s creation plus one tick.

HarnessScoringExpectsLatest verdict
vmmust-passincident: target:graphical-sessionVM: not run in this build

What it exercises

No invariant by name.

desktop-boot-fails-rollback

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A committed change that touches only boot (a kernel parameter) becomes live_tested but not known_good. After a reboot the greeter fails; the boot is not confirmed, Rollback goes to known_good, and after a second reboot the desktop works.

HarnessScoringExpectsLatest verdict
vmrepairboot_default: known_good; generation: known_good; known_good: unchanged; reports: NotBootConfirmed; txn: Committed Rollback; units: service:display-manager RunningVM: not run in this build

What it exercises

I1

desktop-second-user-deadline

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice’s desktop fails; bob logs in 50 s later. alice’s 60 s deadline still fires on time, because it is bound to her session id.

HarnessScoringExpectsLatest verdict
vmmust-passincident: target:graphical-sessionVM: not run in this build

What it exercises

No invariant by name.

disruption-connectivity-alone-seated-runs

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The same rollback by the same seated administrator, but switching back restarts only the tunnel service (ConnectivityLoss). A local caller is exempt from that floor, so the rollback runs and commits; alice’s session is notified.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: known_good; txn: Committed Rollbackfake: PASS

What it exercises

No invariant by name.

disruption-each-kind-keeps-its-floor

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice’s font-scale change commits. An administrator at the seat (their own session, not alice’s) rolls back; switching back restarts a tunnel service (ConnectivityLoss) and a sync service (DataInterruption). The rollback carries both kinds. Being seated exempts the caller from the ConnectivityLoss floor but not from the DataInterruption one, so it asks first and does not run without approval.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: candidate; txn: pending Rollbackfake: PASS

What it exercises

No invariant by name.

drv-bind-fails-restores-previous

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An admin binds the sound card to vfio-pci; probes fail; the transaction unbinds it, rebinds snd_hda_intel, and makes no attempt at a generation rollback.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: base; bound_driver: device:snd-hda0 snd_hda_intel; generation: base; others_unchanged: true; txn: device:snd-hda0 Reverted BindDriver, Revert; units: device:snd-hda0 Boundfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

drv-unbound-audio

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

snd_hda_intel is unbound from the sound card; alice’s pipewire degrades. The root cause is the device, not pipewire; the driver is rebound, and pipewire then recovers.

HarnessScoringExpectsLatest verdict
fake, vmrepairgeneration: base; incident: device:snd-hda0; may_change: device:snd-hda0, service@1000:pipewire, service@1000:wireplumber; others_unchanged: true; txn: device:snd-hda0 Committed BindDriver, service@1000:pipewire Restar…fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

drv-unbound-nic-asks

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

virtio_net is unbound from net-gym0. Rebinding a NIC has ConnectivityLoss disruption, so the daemon’s BindDriver is parked AskFirst; after a TTY approval it runs.

HarnessScoringExpectsLatest verdict
vmrepairothers_unchanged: true; txn: device:net-gym0 Committed BindDriver; units: device:net-gym0 BoundVM: not run in this build

What it exercises

No invariant by name.

dry-activate-exit-nonzero

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The dry activation of a display-scale change exits nonzero. Its plan is unknown, never smaller: the build fails, nothing is activated, and the running generation stays (rule 21, R10).

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; txn: ApplyChangefake: PASS

What it exercises

No invariant by name.

dry-activate-timeout

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The dry activation of a display-scale change times out. Its plan is unknown, never smaller: the build fails, nothing is activated, and the running generation stays (rule 21, R10).

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; txn: ApplyChangefake: PASS

What it exercises

No invariant by name.

dry-activate-truncated

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The dry activation of a display-scale change emits more output than is read (truncated). Its plan is unknown, never smaller: the build fails, nothing is activated, and the running generation stays (rule 21, R10).

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; txn: ApplyChangefake: PASS

What it exercises

No invariant by name.

en-i10-a-canonical-probe-cannot-be-removed

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A configuration change drops sshd’s canonical probe from the manifest (removes it, not weakens it). Below AskFirst it may run, but the probe does not go: the probe definitions the daemon evaluates are the same afterwards, because the core ships the canonical probe and puts it back.

HarnessScoringExpectsLatest verdict
fakemust-passprobe_definitions_unchanged: truefake: PASS

What it exercises

I10

en-i11-session-carries-its-sensitivity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

In one session, an administrator changes the firewall (Security: approved by root at a terminal, committed), then asks for a display-scale change, Routine on its own. The session’s cumulative effect includes the firewall change, so the second is classified at least as high: AskFirst, not as itself.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: Committed ApplyChange, ApplyChangefake: PASS

What it exercises

I11

en-i12-nothing-left-open

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A generation change breaks gym-web, and the revert to the base fails too. The daemon is killed while it is reverting and comes back to finish the revert, which fails again. At the end no journaled transaction is left open, the change is RecoveryFailed with its alert, the interlock holds, and gym-web’s next crash gets no autonomous restart.

HarnessScoringExpectsLatest verdict
fakemust-passinterlock: set; no_txn_on: service:gym-web; nothing_left_open: true; reports: RecoveryFailed; txn: RecoveryFailed ApplyChangefake: PASS

What it exercises

I12

en-i13-overridden-unit-is-not-repaired

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator stops gym-web until they say otherwise. For the next ten minutes gym-web stays stopped and under the override: no repair is proposed for it, and nothing restarts it.

HarnessScoringExpectsLatest verdict
fake, vmmust-passoverrides: service:gym-web; txn: Committed StopAndSuppress, service:gym-web RestartUnit; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

I13

en-i14-forged-fields-ignored

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator who is not root submits a change to sshd whose request also claims caller root, sensitivity Routine and no affected units. The claims are ignored: the change is decided on derived facts (Security, sshd affected), held at AskFirst for approval, and not run.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

I14

en-i15-old-worker-cannot-finish

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The daemon is killed mid-activation and stays down; the watchdog takes the generation change over while the old activation worker is still running. From the takeover on, only the watchdog’s lease advances the transaction or journals a call, the old worker’s activation never overlaps the watchdog’s revert, and each epoch has one owner.

HarnessScoringExpectsLatest verdict
fake, vmmust-passjournal_owner_after_takeover: watchdog; no_overlap: activate_live(candidate), activate_live(revert_to); owners_per_epoch_max: 1fake: PASS; VM: not run in this build

What it exercises

I15

en-i16-boot-fallback-respects-floor

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

After a security update raises the floor, the machine restarts and the boot fails. The automatic fallback does not select a generation below the floor: the boot default and the generation that boots are the floor.

HarnessScoringExpectsLatest verdict
fakemust-passboot_default: candidate; generation: candidate; security_floor: candidatefake: PASS

What it exercises

I16

en-i17-root-administers-without-the-group

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Nobody is left in planarian-admin, so the group grants no one administrator capability. Root still holds it: root’s administrator-only change (gym-web’s listen address) is authorized and commits.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: Committed ApplyChange; units: service:gym-web Runningfake: PASS

What it exercises

I17

en-i18-deleted-record-detected

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A retained journal record is deleted (not edited) and the rest re-framed so every frame is intact. The next read detects the break as an Integrity failure.

HarnessScoringExpectsLatest verdict
fake, vmmust-passposture: class Integrity; reports: JournalChainBrokenfake: PASS; VM: not run in this build

What it exercises

I18

en-i19-no-self-approval

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Root proposes an AskFirst change and then, at a terminal, tries to approve it: refused, and the change never runs. (A principal granting itself a capability has no request to make in Phase A: capabilities come from group membership only.)

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: pending ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

I19

en-i21-irreversible-asks-even-for-root

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Root, at maximum autonomy, asks to restore a snapshot over user data: it cannot be reversed. It is held at AskFirst, whoever asks and whatever the setting.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: RestoreSnapshotfake: PASS

What it exercises

I21

en-i22-user-cannot-do-admin-things

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A seated, unprivileged user tries, in turn, to roll back, collect garbage, bind a driver and resolve the interlock. Each is refused Unauthorized; nothing executes.

HarnessScoringExpectsLatest verdict
fake, vmmust-passno_txn_executed: true; reports: Unauthorized; txn: Rollback, CollectGarbage, BindDriver, Resolvefake: PASS; VM: not run in this build

What it exercises

I22

en-i23-third-party-override-kept

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Between a change’s boot entry being published and its override promotion, a third party rewrites the override file. Planarian does not overwrite it: the third party’s file stands, and the change does not commit (it stops at RecoveryFailed for a person to resolve). Before it, a runtime restart of gym-web wrote no boot entry. On the VM the third party is a cooperating writer: it holds the lock on planarian/ across the daemon’s compare-and-swap and replaces the file before letting go, so the swap waits and then sees the edit (R11).

HarnessScoringExpectsLatest verdict
fake, vmmust-passoverride_file: third-party; txn: RecoveryFailed ApplyChange, service:gym-web Committed RestartUnit; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

I23

en-i5-reject-survives-every-setting

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

With autonomy at its maximum, a change touching Planarian’s own unit is refused, not raised to a tier; with the setting changed and the change asked again, it is refused again. No setting turns the refusal into an approval to ask for.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: ApplyChangefake: PASS

What it exercises

I5

en-i6-repair-waits-for-activation

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

While a slow generation change sits in Activating, gym-web crashes. No repair is prepared until the activation leaves the exclusive states; by the end gym-web runs again.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: service:gym-web RestartUnit; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

I6

en-i8-token-bound-to-candidate

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An AskFirst change is approved; before it runs, the owner edits the configuration so the candidate it would build differs. The token, bound to the old candidate, does not authorise the new one.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: Rejected(Stale) ApplyChangefake: PASS; VM: not run in this build

What it exercises

I8

fake-assumptions-conformance

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The transaction kernel against the fake backend, once per docs/assumptions.md entry and injected outcome. Each run must satisfy the entry’s contract and reach a terminal state (I12).

HarnessScoringExpectsLatest verdict
fakemust-passinvariants: I6, I12, I15; reports: AssumptionHeld(<assumption>)fake: PASS

What it exercises

I6, I12, I15

flap-30s

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-flap fails 30 s after every restart. Three attempts with 10 s / 60 s / 300 s backoff, then a 1 h cooldown. No restart loop.

HarnessScoringExpectsLatest verdict
fake, vmrepairincident: service:gym-flap; metrics: attempt_gaps_min [“10s”, “60s”, “300s”], restarts_max { “service:gym-flap” = 3 }; reports: Flappingfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

flap-regression-escalates

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-flap fails 6 min after each restart, just past the 5 min stabilization. Each failure closes and then regresses the incident, and at the third regression it escalates with a 1 h hold.

HarnessScoringExpectsLatest verdict
fakerepairincident: service:gym-flap; reports: Escalated; txn: service:gym-flapfake: PASS

What it exercises

No invariant by name.

floor-manual-rollback-needs-admin

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

After the security update, an administrator asks to roll back below the floor: AskFirst for an Administrator approver. The proposer cannot approve it (I19); root at a terminal can.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; security_floor: candidate; txn: Committed Rollbackfake: PASS

What it exercises

I16, I19

floor-moves-via-boot-only

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A security update that also needs a reboot takes the boot-only path: published to boot once, the machine restarts, the boot is confirmed and the change commits. The floor moves to it, as on the live path.

HarnessScoringExpectsLatest verdict
fakemust-passsecurity_floor: candidate; txn: Committed ApplyChangefake: PASS

What it exercises

I25

floor-moves-via-watchdog

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The daemon is killed once the security update is Promoted and stays down; the watchdog completes the commit. The floor moves to the fixing generation: the floor record is part of the commit, whoever writes Committed.

HarnessScoringExpectsLatest verdict
fakemust-passsecurity_floor: candidate; txn: Committed ApplyChangefake: PASS

What it exercises

I16, I25

floor-revert-blocked

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A security update (gym-vulnlib 1.0 → 1.1, the old build carries knownVulnerabilities) commits and raises the floor. gym-web then crashes and fails every restart. The only rollback target, known_good, is below the floor: no Rollback runs, the incident escalates with RevertBlockedByFloor, and the boot default stays at the floor.

HarnessScoringExpectsLatest verdict
fakemust-passboot_default: candidate; escalation: RevertBlockedByFloor; generation: candidate; incident: service:gym-web; invariants: I1, I16; security_floor: candidate; txn: Committed ApplyChange, Rollbackfake: PASS

What it exercises

I1, I16

floor-service-package-fix

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A change whose only security fix is in a service’s package (gym-vulnsvc 1.0 -> 1.1; services.gym-vulnsvc.package, never in environment.systemPackages) commits. The security floor moves to it: the inspected set is the configuration’s whole package set, not environment.systemPackages.

HarnessScoringExpectsLatest verdict
vmmust-passsecurity_floor: candidate; txn: Committed ApplyChangeVM: not run in this build

What it exercises

I16

gc-during-approval

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Garbage collection runs while an AskFirst change waits for approval. The candidate survives (its build root is protected), and after approval verify_candidate passes.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgc_protected: candidate, known_good, base; generation: candidate; txn: Committed ApplyChangefake: PASS; VM: not run in this build

What it exercises

I3

gc-retain-all-generations

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

With services.planarian.gc.retainAllGenerations set and the generation budget far below what the generations use, four display-scale changes leave old generations behind; garbage collection then collects none of them (the full history stays), and reports the budget as unsatisfiable rather than enforcing it.

HarnessScoringExpectsLatest verdict
fakemust-passcollected_none: true; reports: BudgetUnsatisfiable; txn: Committed CollectGarbagefake: PASS

What it exercises

I21

gen-bad-switch

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Switching to a prebuilt generation whose gym-web ExecStart is broken: AskFirst (the target was never live-tested), approved, fails its probes, reverted. The candidate never becomes the only boot entry.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: base; generation: base; journal_agrees: true; known_good: unchanged; others_unchanged: true; override_source: base; txn: Reverted SwitchGeneration; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

gen-disable-reenable-canonical

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An admin disables sshd (which has a canonical Readiness probe) and later re-enables it. Disabling makes the probe inapplicable without tampering with it, so neither change is reverted.

HarnessScoringExpectsLatest verdict
fake, vmmust-passjournal_agrees: true; no_incident_on: service:sshd; override_source: candidate; txn: Committed ApplyChange, Committed ApplyChange; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

I10

gen-override-only-change

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A font-scale change touches only overrides.nix. The evaluated output differs from base, Stale is never raised (no owner edit happened), and the change commits.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: candidate; evaluated_option: planarian.reference.fontScale 1.25; generation: candidate; journal_agrees: true; known_good: unchanged; live_tested: candidate; others_unchanged: true; override_source: candid…fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

identity-peer-exited

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice, logged in at the seat, sends a stop of her own pipewire and exits before the daemon resolves her. With no process behind the connection, the daemon attributes the request to uid 1000 with no session: never to a session it cannot read from that process.

HarnessScoringExpectsLatest verdict
vmmust-passtxn: StopAndSuppressVM: not run in this build

What it exercises

I21

intent-cli-stop-rewritten

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An admin issues StopUnit through the CLI; it is rewritten to StopAndSuppress { until: NextBoot }, with a notice.

HarnessScoringExpectsLatest verdict
fake, vmintentno_autonomous_txn: true; overrides: service:gym-web; reports: StopRewrittenNotice; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

intent-start-again-repairs

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

After a user stop, “start gym-web again” lifts the override, and the unit is desired Running again and repaired.

HarnessScoringExpectsLatest verdict
fake, vmrepairoverrides: ; txn: service:gym-web Committed LiftOverride; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

intent-user-stops-service

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An admin asks to stop gym-web until they say otherwise. It stays stopped. The override is journaled before the stop, so there is no window in which a repair can race it.

HarnessScoringExpectsLatest verdict
fake, vmintentjournal_order: Override(service:gym-web), stop(service:gym-web); no_autonomous_txn: true; overrides: service:gym-web; txn: Committed StopAndSuppress; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

I13

journal-compaction-preserves-state

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Compaction is forced while an Explicit override is live and an incident’s budget is exhausted, then both processes crash and restart. Both survive, and replay from checkpoint plus tail equals replay from full history.

HarnessScoringExpectsLatest verdict
fake, vmmust-passincident: service:gym-flap; overrides: service:gym-web; replay_equivalent: truefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

journal-record-edited

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A retained journal record is edited on disk while the daemon runs. The next read detects the broken chain as an Integrity failure; no transaction is taken from the edited record.

HarnessScoringExpectsLatest verdict
fake, vmmust-passinvariants: I18; posture: class Integrity, target_reached planarian-failed@Integrity.target; reports: JournalChainBrokenfake: PASS; VM: not run in this build

What it exercises

I18

journal-torn-write

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A record write is interrupted, and separately a checkpoint write. A torn tail is truncated and never interpreted as a transition; a torn checkpoint never replaces the old one.

HarnessScoringExpectsLatest verdict
fakemust-passjournal_agrees: true; journal_readable: true; replay_equivalent: truefake: PASS

What it exercises

No invariant by name.

login-greeter-idle

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Boot to the greeter with autologin off and leave it idle for 10 min. SessionEstablished never holds, so graphical-session gets no incident.

HarnessScoringExpectsLatest verdict
vmintentno_autonomous_txn: true; no_incident_on: target:graphical-session; units: service:display-manager RunningVM: not run in this build

What it exercises

No invariant by name.

malicious-decl-catalog-enable

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice enables the declared-but-off gym-bad-payload through the service-enable catalog entry (the owner made it user-controllable). The incident names alice, the transaction, and planarian/overrides.nix as the path.

HarnessScoringExpectsLatest verdict
fake, vmmust-passdeclaration_origin: file planarian/overrides.nix, path ApplyChange service-enable, unit service:gym-bad-payload, when txn, who user:1000; incident: service:gym-webfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

malicious-decl-input-update

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A flake input update (the supply-chain path) brings in a module that declares gym-bad-payload. The incident names the input, its lock change (old and new revisions), and who applied the update.

HarnessScoringExpectsLatest verdict
fake, vmmust-passdeclaration_origin: lock_change true, path flake input gym-modules, unit service:gym-bad-payload, when generation-created, who root; incident: service:gym-webfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

malicious-decl-owner-rebuild

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The owner’s flake gains gym-bad-payload and root runs nixos-rebuild switch outside Planarian. The incident on gym-web names gym-bad-payload’s declaration: the owner source file, the out-of-band rebuild as the path, and when that generation appeared.

HarnessScoringExpectsLatest verdict
fake, vmmust-passdeclaration_origin: file owner:configuration.nix, path out-of-band nixos-rebuild, unit service:gym-bad-payload, when generation-created, who root; incident: service:gym-webfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

manifest-weaken-out-of-band

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The owner weakens sshd’s probe in their own config and rebuilds outside Planarian. Canonical probes are still evaluated from the core package, so a later sshd fault is still detected.

HarnessScoringExpectsLatest verdict
vmmust-passcanonical_probe_source: core; status_seen: service:sshd [“Degraded”]VM: not run in this build

What it exercises

I10

manifest-weaken-via-proposal

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A proposal tries to raise sshd’s canonical probe timeout to 600 s and mark it Diagnostic. No catalog entry can express that, so it is rejected and never executed.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgeneration: base; probe_definitions_unchanged: true; txn: ApplyChangefake: PASS; VM: not run in this build

What it exercises

I10

net-external-dependency-down

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The upstream service on the host stops while the gateway is reachable. The incident targets the external unit; there is no local restart.

HarnessScoringExpectsLatest verdict
fake, vmrepairincident: service:external:gym-upstream; no_txn_on: service:gym-upstream-client; units: service:gym-upstream-client Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

net-gateway-down

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Traffic to the gateway is dropped while the NIC driver and NetworkManager are fine. Every probe on the way times out, which is Unknown, never Failed (§10): nothing local is restarted or rebound and no repair is attempted, but Planarian says it can’t reach the upstream. When the block lifts, the client is Running again.

HarnessScoringExpectsLatest verdict
fake, vmrepairno_txn_on: service:gym-upstream-client, service:external:gym-upstream, service:NetworkManager, device:net-gym0; reports: ExternalDependencyUnreachable; status_seen: service:external:gym-upstream Unknown, service:gym-up…fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

net-upstream-down

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The upstream host is up but its service is stopped: connections are refused, a definite failure. The incident targets service:external:gym-upstream, named as the client’s root cause; nothing local is restarted. When the service is back, the incident closes.

HarnessScoringExpectsLatest verdict
fake, vmrepairincident: service:external:gym-upstream; no_txn_on: service:gym-upstream-client, service:NetworkManager, device:net-gym0; reports: ExternalDependencyDown; units: service:gym-upstream-client Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

override-over-attested-refused

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice stops her own pipewire with an override. An administrator then sets a different override on the same unit: Rejected(Precondition), naming alice as the holder; her override stands.

HarnessScoringExpectsLatest verdict
fakemust-passoverrides: service@1000:pipewire; txn: SetOverridefake: PASS

What it exercises

No invariant by name.

override-stopped-equals-stop

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator with no session here (over SSH) asks, in turn, for an override that stops the display manager and for a plain stop of it. Both end the seated user’s session: both derive SessionLoss over the same units and both are held at AskFirst, since it is not the caller’s own session.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: SetOverride, StopAndSuppress; units: service:display-manager Runningfake: PASS; VM: not run in this build

What it exercises

I21

owner-rebuild-breaks-service

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The owner runs nixos-rebuild switch on a config that breaks gym-web. Planarian tries restarts and then escalates; it never rolls back an owner-built generation without AskFirst.

HarnessScoringExpectsLatest verdict
fake, vmrepairgeneration: owner-built; incident: service:gym-web; metrics: restarts_max { “service:gym-web” = 3 }; reports: DiagnoserExhausted; txn: Committed Rollback; units: service:gym-web Failedfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

policy-daemon-session-drop-rebind

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

virtio-gpu is unbound during alice’s session; the diagnoser proposes BindDriver on gpu0. Its disruption is SessionLoss and the proposer is not the session owner, so it is AskFirst, parked and surfaced, and does not run.

HarnessScoringExpectsLatest verdict
fake, vmmust-passreports: AwaitingApproval, DisruptionSessionLoss; txn: device:gpu0 BindDriverfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

policy-daemon-wifi-runtime

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The selected AP (gym-ap-a) goes away while gym-ap-b is known. The diagnoser’s SetRuntimeOption Wi-Fi selection has ConnectivityLoss disruption, so it is AskFirst and does not run.

HarnessScoringExpectsLatest verdict
fake, vmmust-passtxn: SetRuntimeOptionfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

policy-refinement-violation

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A proposal’s expectation broadens may_change beyond what spec.predict() allows, and the refinement check rejects it.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: RestartUnitfake: PASS

What it exercises

No invariant by name.

policy-stale-digest

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A proposal carries an observed digest from before a state change. It is rejected as Stale, and the current digest is returned.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: RestartUnitfake: PASS

What it exercises

No invariant by name.

posture-absent-masked

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad is masked and the machine rebooted. planarian-ready.target times out; Standard’s Absent responses apply; the agent stand-in never starts; unmasking and a clean ready lifts them.

HarnessScoringExpectsLatest verdict
vmmust-passinvariants: I20; not_started: service:gym-agent; posture: class Absent, not [“restrict_login”, “halt_to_recovery”, “alarm_dismissable”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target…VM: not run in this build

What it exercises

I20

posture-crashed-budget

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planariad reaches ready, then is killed until its restart budget is spent. OnFailure= reaches planarian-failed@Crashed; responses as Absent; agents stop; an administrator’s acknowledgement is journaled with identity.

HarnessScoringExpectsLatest verdict
vmmust-passinvariants: I20; posture: class Crashed, not [“restrict_login”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Crashed.target; reports: AlarmAcknowledged(use…VM: not run in this build

What it exercises

I20

posture-integrity-core-hash

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The recorded core hash no longer matches its signature (I2). planarian-recovery’s independent check fails the boot as Integrity: tamper alarm, notify, no agents, remote access off; local login proceeds with the warning. Only an administrator clears it.

HarnessScoringExpectsLatest verdict
vmmust-passinvariants: I2, I20; not_started: service:gym-agent; posture: class Integrity, not [“halt_to_recovery”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integr…VM: not run in this build

What it exercises

I2, I20

posture-integrity-core-script

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

One posture script (planarian-ack), not planariad, no longer matches its line in the core-hash record. planarian-recovery checks every Planarian binary and script, so the boot fails as Integrity all the same (B-12).

HarnessScoringExpectsLatest verdict
vmmust-passinvariants: I2; posture: class Integrity, target_reached planarian-failed@Integrity.targetVM: not run in this build

What it exercises

I2, I20

posture-integrity-journal-chain

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A record in the hash-chained journal is altered while the machine is off. The boot report finds the broken chain (I18): Integrity responses, as above.

HarnessScoringExpectsLatest verdict
vmmust-passinvariants: I18, I20; posture: class Integrity, responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integrity.target; reports: BootReport(JournalChainBroken)VM: not run in this build

What it exercises

I18, I20

posture-integrity-undeclared-early-unit

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A unit outside the generation’s closure is planted to start before planarian-ready. The boot report names it and Integrity responses apply. (Phase A’s boot report checks units only, and planariad stops on the finding rather than opening an incident: catalog review G18.)

HarnessScoringExpectsLatest verdict
vmmust-passposture: class Integrity, responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”], target_reached planarian-failed@Integrity.target; reports: BootReport(UndeclaredEarly: service:gym-planted)VM: not run in this build

What it exercises

I20

posture-stops-launched-agents

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An administrator launches an agent with planarian agent run (in planarian-agents.slice). planariad then crash-loops past its restart budget: planarian-failed@Crashed stops the agents’ slice as a whole, so the launched agent stops too, not only the declared agent units.

HarnessScoringExpectsLatest verdict
vmmust-passinvariants: I20; posture: class Crashed, responses [“disable_agents”], target_reached planarian-failed@Crashed.target; unit_after_failure: service:planarian-agent-launched StoppedVM: not run in this build

What it exercises

I20

probe-delayed-healthy

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web’s /health answers after 15 s, past the 10 s timeout, while the service is fine. The status is Unknown, never Failed. After three Unknowns a diagnostic incident re-observes through the Tcp probe and does not restart.

HarnessScoringExpectsLatest verdict
fake, vmintentincident: service:gym-web; no_txn_on: service:gym-web; status_never: service:gym-web [“Failed”]; status_seen: service:gym-web [“Unknown”]fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

probe-diagnostic-stale-healthy

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The evidence of the audio tone probe (Diagnostic, OnDemand) expires on a healthy machine. Status stays Running and no incident opens.

HarnessScoringExpectsLatest verdict
fake, vmintentno_autonomous_txn: true; no_incident_on: service@1000:pipewire; units: service@1000:pipewire Runningfake: PASS; VM: not run in this build

What it exercises

I4

probe-failed-beats-stale

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web’s Http probe is made stale, then the process is killed: status is Failed, not Unknown. gym-app’s probe is stale at the same time and its status is Unknown.

HarnessScoringExpectsLatest verdict
fake, vmmust-passincident: service:gym-web; status_never: service:gym-app [“Failed”]; status_seen: service:gym-app [“Unknown”], service:gym-web [“Failed”]fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

probe-http-503-degraded

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web is active, but /health returns 503: the status is Degraded, an incident opens, and a restart clears the runtime fault.

HarnessScoringExpectsLatest verdict
fake, vmrepairothers_unchanged: true; status_seen: service:gym-web [“Degraded”]; txn: service:gym-web Committed RestartUnit; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

probe-trickling-endpoint

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web’s /health sends one byte every 2 s and never finishes, so a per-read timeout would never fire. Its 10 s timeout bounds the whole probe: Timeout, Unknown, never Failed, and gym-web is not restarted. Meanwhile the loop keeps ticking: gym-cache crashes and is repaired.

HarnessScoringExpectsLatest verdict
vmmust-passno_txn_on: service:gym-web; status_never: service:gym-web [“Failed”]; status_seen: service:gym-web [“Unknown”]; txn: service:gym-cache Committed RestartUnit; units: service:gym-cache RunningVM: not run in this build

What it exercises

No invariant by name.

quiet-reference-workload

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The invisibility-budget benchmark (§21): the reference game runs while a GC, a staged update and a non-urgent fault are queued. p99 frame time and input latency match the Planarian-stopped baseline; each deferred item runs in its window afterward; a PipeWire crash mid-run is repaired with one notice.

HarnessScoringExpectsLatest verdict
benchmust-passmetrics: deferred_items_ran 3, frame_time_p99 no significant difference vs baseline; notices_count: count 1, match pipewire

What it exercises

No invariant by name.

quiet-repair-not-deferred

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

While a game holds Foreground, alice’s PipeWire crashes. Repairs of what foreground work depends on are not deferred: it is repaired at once, with exactly one notice.

HarnessScoringExpectsLatest verdict
fakerepairnotices_count: count 1, match pipewire; units: service@1000:pipewire Runningfake: PASS

What it exercises

No invariant by name.

quiet-staged-update-foreground

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice changes her font scale while a game holds Foreground in her session. The verified candidate waits in Built; once Foreground ends it activates in that window and commits.

HarnessScoringExpectsLatest verdict
fakemust-passreports: WaitsForWindow; txn: Committed ApplyChangefake: PASS

What it exercises

I6

release-refused-leaves-incident

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web keeps failing until its incident has spent its repair budget and escalated. alice, who may not act on gym-web, asks to release it: refused Unauthorized, and the incident record is unchanged (no budget restored, no attempt brought forward, not reopened).

HarnessScoringExpectsLatest verdict
fake, vmmust-passincident_unchanged_by_refusal: service:gym-web; txn: service:gym-web LiftOverridefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

reprepared-keeps-caller

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice changes her font scale while a game holds Foreground in her session, so it waits in Built. The daemon restarts: the change is prepared again as alice’s, in her session (not the daemon’s), and commits once Foreground ends.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: Committed ApplyChangefake: PASS

What it exercises

No invariant by name.

residue-first-kill-teaches-stop

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A user’s first raw kill -9 of gym-web is caught as the unattributed residue; its notice names planarian stop service:gym-web as the attributed way. A later raw stop of gym-db is inferred the same way, without the lesson again.

HarnessScoringExpectsLatest verdict
fakeintentnotices_count: , , ; overrides: service:gym-web, service:gym-db, service:gym-app; reports: InferredOverride, Undofake: PASS

What it exercises

No invariant by name.

residue-infer-stops-false

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A raw kill -9 of gym-backup, whose manifest sets inferStops = false, under Standard: never taken as intended; alarmed and started again, as a fault.

HarnessScoringExpectsLatest verdict
fake, vmmust-passoverrides: ; reports: alarm; units: service:gym-backup Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

residue-inferred-listed-until-ack

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An inferred override (gym-web killed under Standard) is listed until a user acknowledges it with planarian ack; acknowledging stops the listing and does not lift the override.

HarnessScoringExpectsLatest verdict
fake, vmintentinferred_listed: ; no_txn_on: service:gym-web; overrides: service:gym-web; reports: InferredOverride; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

residue-kill9-security

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A raw kill -9 of sshd (Security-sensitive) is a fault under every posture: alarmed, never inferred, and back up: restored by Planarian on its own (§11; catalog-review G16, decided 2026-10-29), or by systemd where its Restart= does it first (NixOS’s sshd), which is then alarmed from systemd’s restart count.

HarnessScoringExpectsLatest verdict
fake, vmmust-passoverrides: ; reports: alarm; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

I22

residue-kill9-service

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A raw kill -9 of gym-web (Restart=no, not Security-sensitive). Under Standard it is accepted until next boot as an inferred, unattributed override, with a notice and an undo. No restart.

HarnessScoringExpectsLatest verdict
fake, vmintentno_txn_on: service:gym-web; overrides: service:gym-web; reports: InferredOverride, Undo; units: service:gym-web Overriddenfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

residue-kill9-undo

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Continuation: alice uses the offered undo. The inferred override is lifted by an authorized principal and gym-web is started.

HarnessScoringExpectsLatest verdict
fakemust-passoverrides: ; txn: LiftOverride; units: service:gym-web Runningfake: PASS

What it exercises

No invariant by name.

residue-oom-kill

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web is driven past MemoryMax and the kernel kills it (Result=oom-kill). OOM kills are always faults: restarted, no inferred override.

HarnessScoringExpectsLatest verdict
fake, vmrepairoverrides: ; txn: service:gym-web RestartUnit; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

residue-stop-security-unit

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Root stops sshd (Security-sensitive) with systemctl stop outside Planarian. An unattributed stop of a Security unit is never taken as intended: it is restored on its own and alarmed.

HarnessScoringExpectsLatest verdict
fake, vmrepairoverrides: ; reports: alarm; txn: service:sshd RestartUnit; units: service:sshd Runningfake: PASS; VM: not run in this build

What it exercises

I22

revert-fails

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Bad candidate, while an environmental fault (sshd’s port blocked) makes the revert probes fail too. Result: RecoveryFailed, an alert, and the interlock, which blocks autonomous repair of a later unrelated fault until an admin resolves it.

HarnessScoringExpectsLatest verdict
fake, vmmust-passinterlock: clear; reports: RecoveryFailed, alert; txn: RecoveryFailed ApplyChange, service:gym-cache RestartUnit; units: service:gym-cache Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

revert-fails-interlock-survives-compaction

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A failed revert, then forced compaction, then both processes restart. Autonomous mutation stays blocked until the resolution is durably recorded.

HarnessScoringExpectsLatest verdict
fake, vmmust-passinterlock: set; no_autonomous_txn: true; replay_equivalent: true; units: service:gym-cache Failedfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

revert-to-older-sets-no-floor

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-newlib 1.0 is added, then a release makes it 2.0, which carries knownVulnerabilities. Undoing back to the 1.0 generation is a Revert to an older generation that merely lacks the newer package: it sets no security floor (§12e).

HarnessScoringExpectsLatest verdict
fakemust-passsecurity_floor: none; txn: Committed Revertfake: PASS

What it exercises

I25

rollback-repair-foreground-dependency

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice’s font-scale update commits; then, while a game holds Foreground in her session, the new generation’s NetworkManager fails and restarts cannot fix it. Wi-Fi is something foreground work depends on, so the diagnoser’s Rollback is a repair: it runs at once, during the workload, never parked in Built for a window, and Wi-Fi is back. One notice about it.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: known_good; incident: service:NetworkManager; notices_count: count 1, match NetworkManager: roll back to the known-good generation; txn: Committed ApplyChange, Committed Rollback; units: service:NetworkMana…fake: PASS

What it exercises

No invariant by name.

rollback-repair-foreground-unrelated

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The same committed update, but the new generation breaks the print service, which foreground work does not depend on. While the game holds Foreground the repair is deferred in fact: no Rollback (and no other repair) is submitted, no transaction is opened, and the incident’s budget does not move. When the game ends the deferred repair runs: in-place repair fails, the Rollback runs and the printer is back.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: known_good; incident: service:gym-printer; txn: Committed Rollback, Rollback, ; units: service:gym-printer Runningfake: PASS

What it exercises

No invariant by name.

rollback-repair-session-loss-asks

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice’s font-scale update commits; then the new generation breaks the print service and restarts cannot fix it. Switching back would also restart the display manager, which hosts her active session’s compositor, so the diagnoser’s Rollback would end her session: being a repair exempts it from waiting, never from asking. It is AskFirst (SessionLoss; the diagnoser is not the session’s own user) and, with nobody approving, it never runs: the update stays live.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: candidate; txn: pending Rollback; units: service:display-manager Runningfake: PASS

What it exercises

No invariant by name.

session-logout-one-of-two

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice logs out of one of her two sessions. Her user services (pipewire) keep running; no incident, no transaction.

HarnessScoringExpectsLatest verdict
fakeintentno_autonomous_txn: true; no_incident_on: service@1000:pipewire; units: service@1000:pipewire Runningfake: PASS

What it exercises

No invariant by name.

session-second-compositor-fails

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice has two graphical sessions. The compositor in the second never runs; an incident opens for that session only, though her user manager’s graphical-session.target is already reached.

HarnessScoringExpectsLatest verdict
fakemust-passincident: target@1000/s2:graphical-session; no_incident_on: target@1000/s1:graphical-sessionfake: PASS

What it exercises

No invariant by name.

ssh-session-expects-no-desktop

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice logs in over SSH only (a user session with no seat) and stays for 5 min. Her user services are desired, but no compositor is expected: graphical-session has no instance for that session and no incident opens.

HarnessScoringExpectsLatest verdict
fakemust-passno_incident_on: target:graphical-session; units: service@1000:pipewire Runningfake: PASS

What it exercises

No invariant by name.

stale-owner-edit-during-build

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

During the build, the owner edits a tracked file without committing it. The transaction is Rejected(Stale), never activated, and its GC root is released.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgc_root_released: true; generation: base; override_source: base; txn: Rejected(Stale) ApplyChangefake: PASS; VM: not run in this build

What it exercises

I23

stale-untracked-file-ignored

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

During the build, the owner adds an untracked file to the flake. Untracked files are not visible to Nix: no Stale, and the file is reported.

HarnessScoringExpectsLatest verdict
vmmust-passgeneration: candidate; reports: UntrackedExcluded; txn: Committed ApplyChangeVM: not run in this build

What it exercises

No invariant by name.

storage-boot-full-publish

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

/boot is full when publish_boot runs. This is treated as a Checking failure, so the change reverts, and the candidate is never the only boot entry.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: base; generation: base; journal_agrees: true; override_source: base; txn: Reverted ApplyChangefake: PASS; VM: not run in this build

What it exercises

No invariant by name.

storage-budget-unsatisfiable

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The generation budget is set below the size of the protected set. GC collects everything unprotected, reports BudgetUnsatisfiable with the shortfall, and never touches the protected set.

HarnessScoringExpectsLatest verdict
fake, vmmust-passgc_protected: known_good, base, previous; reports: BudgetUnsatisfiablefake: PASS; VM: not run in this build

What it exercises

I3

storage-enospc-during-build

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The Nix store fills during build: Rejected(NoSpace), a storage incident opens, and the original change is not retried until that incident closes.

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; incident: device:fs-nix; txn: Rejected(NoSpace) ApplyChangefake: PASS

What it exercises

No invariant by name.

storage-journal-btrfs-reuse

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

storage-journal-fs-full again on btrfs, after a snapshot and after 1000 journal wraparounds (nodatacow reuse).

HarnessScoringExpectsLatest verdict
vmmust-passjournal_readable: true; one_of: , ,VM: not run in this build

What it exercises

No invariant by name.

storage-journal-fs-full

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The journal filesystem is filled to the reserved-block boundary as the daemon’s uid. Either emergency collection is recorded and executed, or mutation stops with an explicit NoSpace or RecoveryFailed. A journal record is never lost or misread.

HarnessScoringExpectsLatest verdict
vmmust-passjournal_readable: true; one_of: , , ; replay_equivalent: trueVM: not run in this build

What it exercises

No invariant by name.

storage-nix-full-gc

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The Nix store filesystem drops below max(2 GB, 5%) while old generations exist. CollectGarbage runs first, and the protected set is untouched.

HarnessScoringExpectsLatest verdict
fake, vmrepairgc_protected: known_good, base, previous; txn: Committed CollectGarbage; units: device:fs-nix Runningfake: PASS; VM: not run in this build

What it exercises

I3

suspend-resume

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

S3 suspend for 2 min, then resume. Evidence goes stale while suspended (Unknown, not Failed), nothing is restarted, and everything is Running within settle.

HarnessScoringExpectsLatest verdict
fakeintentno_autonomous_txn: true; others_unchanged: true; units: service:NetworkManager Running, service:gym-web Running, service@1000:pipewire Runningfake: PASS

What it exercises

No invariant by name.

svc-killed

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web is SIGKILLed; it has no Restart=; the diagnoser restarts it.

HarnessScoringExpectsLatest verdict
fake, vmrepairincident: service:gym-web; may_change: service:gym-web; metrics: restarts_max { “service:gym-web” = 1 }; others_unchanged: true; settle: ready service:gym-web, timeout 60s; txn: service:gym-web Committed RestartUnit; u…fake: PASS; VM: not run in this build

What it exercises

No invariant by name.

svc-killed-defer-to-systemd

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-restartful is killed; systemd restarts it itself; Planarian does not act.

HarnessScoringExpectsLatest verdict
vmintentno_txn_on: service:gym-restartful; others_unchanged: true; units: service:gym-restartful RunningVM: not run in this build

What it exercises

No invariant by name.

svc-killed-user-scope

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice’s pipewire is killed while her session is up; the user-scoped unit is restarted.

HarnessScoringExpectsLatest verdict
vmrepairmay_change: service@1000:pipewire, service@1000:wireplumber; others_unchanged: true; settle: ready service@1000:pipewire, timeout 60s; txn: service@1000:pipewire Committed RestartUnit; units: service@1000:pipewire Runn…VM: not run in this build

What it exercises

No invariant by name.

svc-misconfig-listen-address

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The user sets gym-web’s listen address to 192.0.2.10, a permitted value that no interface holds. The change is applied as a transaction, the probes fail, and the change is reverted.

HarnessScoringExpectsLatest verdict
fake, vmmust-passboot_default: base; generation: base; journal_agrees: true; known_good: unchanged; others_unchanged: true; override_source: base; txn: Reverted ApplyChange; units: service:gym-web Runningfake: PASS; VM: not run in this build

What it exercises

No invariant by name.

svc-misconfig-state-file

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

gym-web’s mutable state file is corrupted, so it fails on every start. Restarting cannot fix it: three attempts with backoff, then escalation with a report. Phase B upgrades required_post to Running.

HarnessScoringExpectsLatest verdict
vmrepairgeneration: base; incident: service:gym-web; metrics: attempt_gaps_min [“10s”, “60s”], restarts_max { “service:gym-web” = 3 }; others_unchanged: true; reports: DiagnoserExhausted; units: service:gym-web FailedVM: not run in this build

What it exercises

No invariant by name.

switch-facts-from-dry-activation

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A prebuilt generation has the running one’s manifest, but its NetworkManager unit has a new store path. Switching to it has the facts its dry activation shows: NetworkManager restarts, so it is ConnectivityLoss and asked first, as the same switch built as a candidate would be. Nobody approves, so nothing changes (§11 equivalent effects, R04).

HarnessScoringExpectsLatest verdict
fakemust-passgeneration: base; others_unchanged: true; txn: SwitchGenerationfake: PASS

What it exercises

No invariant by name.

test-build-is-integrity

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The test build (gym-hooks, which accepts fault injection over its socket) starts with a configuration that does not allow it, as it would anywhere outside the gym’s VM tests. planariad refuses to run: an Integrity failure, with exactly the Integrity responses and no agent started. (The module refuses such a configuration at evaluation; checks.module-eval covers that.)

HarnessScoringExpectsLatest verdict
vmmust-passnot_started: service:gym-agent; posture: class Integrity, not [“halt_to_recovery”], responses [“alarm”, “notify_admins”, “disable_agents”, “restrict_remote”]VM: not run in this build

What it exercises

No invariant by name.

token-wall-clock-moved-back

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

An approval token is held, then the wall clock is moved back 48 h and 24 h pass. The token still expires 24 h after issue, timed on CLOCK_BOOTTIME (§12h): presenting it is refused and the parked change ends Rejected.

HarnessScoringExpectsLatest verdict
fakemust-passtxn: ApplyChangefake: PASS

What it exercises

I8

unauthorized-change-does-no-work

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice, who may not change the configuration beyond her own settings, asks to disable sshd: refused Unauthorized before any source is staged, evaluated or built.

HarnessScoringExpectsLatest verdict
fakemust-passno_build_work: true; txn: ApplyChange; units: service:sshd Runningfake: PASS

What it exercises

No invariant by name.

user-unit-after-logout

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

alice logs out and her pipewire stops. A user unit is desired only while its session exists, so no incident opens.

HarnessScoringExpectsLatest verdict
vmintentno_autonomous_txn: true; no_incident_on: service@1000:pipewire, target:graphical-sessionVM: not run in this build

What it exercises

No invariant by name.

Source files

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Which invariants and obligations live in each file.

crates/exec/src/kani.rs

ObligationKindLineInvariants
exec::approval_tokens_expire_after_their_lifetimeKani harness14I8
exec::residue_security_is_never_intendedKani harness94I13
exec::residue_crashes_are_faultsKani harness105I13
exec::residue_is_inferred_only_where_allowedKani harness116I13

crates/policy/src/lib.rs

ObligationKindLineInvariants
policy::decideverified fn869I5
policy::may_approveverified fn923I19
policy::may_grantverified fn942I19
policy::is_administratorverified fn1014I17

crates/policy/src/proofs.rs

ObligationKindLineInvariants
policy::i5_autonomy_never_touches_rejectproof fn73I5
policy::i11_non_routine_session_asksproof fn95I11
policy::i11_session_only_raisesproof fn118I11
policy::global_floorsproof fn129I21
policy::own_session_is_per_sessionproof fn143I21
policy::i16_below_floor_needs_administratorproof fn161I16
policy::administrator_class_only_on_ask_firstproof fn170I16
policy::security_override_held_for_administratorproof fn179I22
policy::security_restart_runs_alone_only_to_restoreproof fn196I22
policy::i19_no_self_approvalproof fn215I19
policy::agents_and_unattributed_are_refusedproof fn227I22
policy::i10_canonical_probe_changes_askproof fn237I10
policy::unauthorized_is_refused_firstproof fn246I5
policy::core_is_never_a_targetproof fn255I5
policy::only_administrators_resolveproof fn266I22
policy::users_cannot_change_generationsproof fn276I22
policy::i14_facts_caller_is_not_authorityproof fn290I14
policy::i17_administrators_never_emptyproof fn327I17

crates/txn/src/chain.rs

ObligationKindLineInvariants
txn::followsverified fn97I18
txn::verifyverified fn120I18
txn::next_linkverified fn168I18
txn::appending_keeps_the_chainproof fn182I18
txn::an_edited_record_breaks_the_chainproof fn214I18

crates/txn/src/kani.rs

ObligationKindLineInvariants
txn::recovery_table_follows_the_state_machineKani harness185I12
txn::watchdog_takes_over_only_when_it_shouldKani harness199I12
txn::takeover_fences_out_the_previous_ownerKani harness225I15
txn::override_swap_never_overwrites_a_third_partyKani harness261I23

crates/txn/src/ledger.rs

ObligationKindLineInvariants
txn::compatible_execverified fn460I6
txn::well_formedverified fn534I6
txn::validateverified fn607I6, I15
txn::applyverified fn915I6, I12
txn::add_interlockverified fn934I12
txn::i6_one_generation_activeproof fn1235I6

crates/txn/src/machine.rs

ObligationKindLineInvariants
txn::step_allowedverified fn61I12
txn::i12_steps_terminateproof fn130I12
txn::i12_terminal_always_reachableproof fn141I12
txn::terminal_is_finalproof fn151I12
txn::refusal_only_before_mutationproof fn161I12
txn::runtime_never_revertsproof fn171I23
txn::only_generations_publishproof fn182I6
txn::boot_only_never_activatesproof fn194I6
txn::generations_commit_after_promotionproof fn206I23

API docs

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

cargo doc for the workspace. Proof obligations link to source lines instead: rustdoc renders verus! blocks poorly.

§1. What it is, in one page

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Most operating systems can tell you that something broke. Planarian is built to know what should be true, notice when it isn’t, and close the gap without making things worse.

It can do this because the system is declared: a NixOS configuration says which services, drivers, and settings should exist. From that, plus a manifest that adds what NixOS doesn’t say (what depends on what, how to test each thing, how risky it is to touch), Planarian derives desired state. Desired state has conditions: the graphical session should be running after someone logs in; a printer service should be running when the printer is connected; a service the user stopped on purpose is left alone until they say otherwise. Planarian observes actual state continuously and runs health probes under a strict contract (no sudden noises, no side effects, bounded cost). When a probe times out, the answer is Unknown, not Failed.

A gap between desired and observed state opens an incident. Repair follows a strict order:

  1. Root-cause. The dependency graph distinguishes “must be up for this to work” from “merely starts before” from “we suspect these are related.” A failed audio service whose required sound-card driver also failed is a symptom; the driver is the target.
  2. Deterministic search. A small planner searches the known transitions (restart this, rebind that, apply a cataloged change, roll back) for one whose preconditions hold and whose predicted result closes the gap, safest and least disruptive first.
  3. Summon the model. Only if search finds nothing, or the incident’s budget is spent, does a local model get the state, the graph, what was tried, and the manuals. It proposes in the same typed format the planner uses.
  4. Escalate. If that also fails, the user gets a plain-language report, and the incident goes quiet with a cooldown rather than retrying forever.

User requests follow the same order: “reconnect Wi-Fi” matches a declared action directly; “stop the backup service while I work on it” becomes an attributed override of desired state, so Planarian doesn’t fight the user; “make it faster” is ambiguous and goes to the model, which asks before proposing.

Whoever proposes, one executor decides, and it decides from facts it derives itself: what the change actually touches in the evaluated configuration, how sensitive and how disruptive that is, what else changed this session. Nothing in a proposal can grant authority. Runtime actions with no lasting effect just run. Configuration changes run as a transaction: journaled before anything happens, activated as a candidate generation, probed, and either committed or reverted, with a watchdog that finishes the job if Planarian itself is killed halfway. Security-relevant, irreversible, or user-data changes wait for approval, and the approval is re-checked against the exact change the moment before it runs. The repair machinery itself can never be changed by any proposal, under any approval.

The model is a convenience and a fallback. If it is absent, broken, or wrong, the system still boots, runs, repairs the easy faults, and reverts the bad changes.

Planarian is meant to be unnoticed under normal use. Its overhead, its notifications, and its effect on a game’s frame time are measured, and none of them may get worse between releases. It defers non-urgent work while the user is absorbed in something, and it never undoes a deliberate choice; it only needs the choice to have a name on it.

§2. Component diagram

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

┌────────────────────────────────────────────────────────────────────────┐
│  USER SURFACES          console · `ask` CLI · launcher box             │
└──────────────┬──────────────────────────────────────────┬──────────────┘
               │ requests                                 │ approvals (secure path)
               ▼                                          ▼
┌────────────────────────┐                     ┌─────────────────────────┐
│  INTENT MATCHER        │  unmatched / fuzzy  │  APPROVAL BROKER        │
│  phrases → transitions ├────────────┐        │  tokens bound to the    │
│  or desired-state      │            │        │  concrete candidate     │
│  overrides             │            ▼        └────────────┬────────────┘
└──────────────┬─────────┘   ┌──────────────────┐           │ token
               │ proposal    │  MODEL SERVICE   │           │
               │             │  unprivileged;   │           │
               │             │  emits proposals │           │
               │             └────────┬─────────┘           │
               ▼                      ▼ proposal            ▼
┌────────────────────────────────────────────────────────────────────────┐
│  CORE                                                                  │
│  ┌───────────┐ ┌───────────┐ ┌─────────┐ ┌──────────────┐ ┌─────────┐ │
│  │ INCIDENT  │▶│ DIAGNOSER │▶│ POLICY  │▶│ EXECUTOR     │▶│ JOURNAL │ │
│  │ lifecycle │ │ search    │ │ derives │ │ transaction  │ │ durable │ │
│  │ budgets   │ │ over graph│ │ facts;  │ │ protocol     │ │ states  │ │
│  │ cooldowns │ │           │ │ decides │ │              │ │         │ │
│  └─────▲─────┘ └───────────┘ └─────────┘ └──────┬───────┘ └────▲────┘ │
│        │ divergences                            │               │      │
└────────┼────────────────────────────────────────┼───────────────┼──────┘
         │                                        ▼               │
┌────────┴───────────────────┐   ┌──────────────────────────┐ ┌──┴──────────┐
│  STATE                     │   │  BACKENDS (swappable)    │ │ RECOVERY    │
│  desired (conditions,      │◀──│  units: systemd          │ │ WATCHDOG    │
│   overrides)               │   │  config: nixos-rebuild   │ │ separate    │
│  observed (unknown, stale) │   │  snapshots: btrfs/ZFS    │ │ unit; reads │
│  health (probe contract)   │   │  inventory: udev/sysfs   │ │ the journal │
│  graph (typed edges)       │   │  kernel: Linux           │ └─────────────┘
└────────────────────────────┘   └──────────────────────────┘
                                            │
                             ┌──────────────┴───────────────┐
                             │  TRAINING GYM (offline)      │
                             │  faults, lifecycle cases,    │
                             │  disruption metrics          │
                             └──────────────────────────────┘

Arrows into the core carry proposals. Only the executor calls backends. The model has exactly one write capability: submit a proposal. The recovery watchdog is a separate process that can finish or revert a transaction when the daemon is gone. Everything drawn above the CORE box is a principal (§7, §11); the core’s response to its own failure is enforced by systemd and the initrd, not by the core (§12d).

§3. Feature overview

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

FeatureWhat the user seesStatus at v1 (NixOS service)
Fault detection“audio: Failed (probe tone-playback)” within seconds; “Unknown” when evidence is staleYes
Deterministic repairRestart-and-rebind faults fixed with no model, logged with reasonYes
Incident disciplineNo restart loops: cooldowns, backoff, flap detection, cooperation with systemd’s own restartsYes
Respecting intentA service the user stopped stays stopped; conditional desired state (after login, when device present)Yes
Durable transactionsA config change survives Planarian being killed mid-activation; the watchdog commits or revertsYes
Automatic revertFailed probes after activation revert without the userYes
Model-assisted repairHarder faults diagnosed by a local 4B model; proposals previewedPhase B
Natural-language operation“reconnect wifi”, “make text bigger” → matched action or cataloged change with previewYes
Risk tiers and approvalSecurity, external, user-data changes wait; approval re-validated at executionYes (CLI approval in A, root at a terminal; broker in C)
Disruption awarenessActions that would drop your session or connection are classed and surfaced as suchYes
SnapshotsSnapshot before every switch; restore on request; space-budgetedIf btrfs or ZFS present
Boot-confirmed generationsA kernel change is not “good” until it has rebootedYes
Visual consoleLive graph of desired vs observed vs health, timeline, generations, incidentsYes (read-only in A)
Degraded modeEverything but model-assisted repair and fuzzy requests works with no modelYes
Attributed actionsEvery deliberate stop, disable, or disconnect is recorded with who did it, before it happens; nothing deliberate is undoneYes
Security floorAutomatic revert never goes back below a generation that fixed a security problemYes
Failure postureIf Planarian itself fails or the system shows signs of tampering, the machine responds as declared: alarm, agents off, restricted access, or haltYes (Standard posture in A; all postures in C)
Quiet modeNothing non-urgent runs while a game or fullscreen app is in the foreground; updates wait for idleYes
Sandbox tierProposed changes tested first in a disposable Firecracker microVMPhase B

§4. Roadmap

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Each phase ships something usable on its own. Later phases replace a backend; the contract does not change.

PhaseDeliverableGate to exit
A · Contract on NixOS 26.05planariad: schema, state model (desired/observed/health/graph), policy, executor with hash-chained transaction journal and recovery watchdog, systemd and nixos-rebuild backends, change catalog, deterministic diagnoser and incident manager, intent matcher, ask CLI, read-only visual console, fault-injection gym with lifecycle cases. Attributed overrides and session-aware actors (§8, §11); security floor (§12e); failure-posture mechanism with the Standard posture (§12d); activation scheduling and quiet mode (§12f, §10); boot report (§10); signed external desired-state source and signed health report as hooks (§9, §10). Gym also runs non-gating against an Asterinas NixOS VM and a Firecracker microVM target (allowed to stay red; no engineering time beyond keeping them running). schema, policy, and the transaction kernel in the Verus subset. No model.GA: (1) the demonstration: inject a fault → repaired; inject a bad configuration → reverted; kill planariad during activation → watchdog recovers; restart the daemon → it reconciles the journal and explains what happened accurately. (2) ≥ 60% of the catalog cleared deterministically; every bad generation reverted; the manifest-weakening fault rejected; zero gamed successes; unnecessary-intervention rate on the intent cases = 0; every posture and security-floor gym case passes. (3) policy proofs pass in CI. (4) Baselines reported: the invisibility budget (§21) and the verification ratio d:a with the trust count (§17a).
B · The modelModel service with residency rules (§20); boot-model fine-tune via distillation; summoned only on diagnoser exhaustion; evaluation suite with held-out fault families. Sandbox tier: dry-run and Firecracker-from-snapshot (§12g); a passing sandbox run is required for Tested changes where KVM is available. Non-human principals may hold catalog and model-catalog authority (§11).GB: diagnoser + model clear ≥ Y% including held-out families; a worse model is rejected by the suite; no model load while Foreground holds; invisibility budget and d:a not degraded; the trust count not increased.
C · PlanarianOSPlanarianOS: a NixOS distribution in which Planarian governs the machine from first boot (§9a), with three profiles: agent host, server / fleet node, personal. Installer selects profile and failure posture; all four postures (§12d); approval broker; snapshot backend; hardware inventory and explain; Flatpak as the user-scope app valve. The personal profile ships the COSMIC reference desktop and the console (a libcosmic app with approvals) as an optional frontend. Planarian also remains a standalone NixOS module and flake. Real users feed the gym.GC: per-profile gates (§9a); ten external users for a month; every reported fault reproduced as a catalog entry; invisibility budget and d:a not degraded; the trust count not increased.
D · Rust system layerSnix-based evaluator and store; Rust init and supervisor; Rust CoW filesystem. Linux kernel. Backend contracts revised from what A–C taught.GD: no systemd, no C Nix; full catalog passes; d:a not degraded and the trust count not increased (§17a).
E · AsterinasKernel swap; reference-laptop drivers; Smithay desktop; secure attention key; Verus on the core.GE: reference laptop boots, suspends, connects, plays audio, runs the desktop, survives the catalog from cold boot; d:a not degraded and the trust count not increased.
F · AccelerationGPU drivers, model on GPU, COSMIC migration, second machine, ARM64.GF: accelerated desktop and model on both machines; planner correct under injected accelerator failure.

Firmware: binary vendor firmware is loaded as data by Rust drivers and is allowed permanently.


Part II — Specification for implementation

Normative unless marked draft. MUST/MUST NOT are requirements; SHOULD is a default the agent may deviate from with a logged reason in docs/deviations.md.

§5. Scope and non-goals

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Phase A in scope: everything in the Phase A roadmap row, on NixOS 26.05 with systemd, D-Bus, udev; btrfs/ZFS optional; root via a dedicated systemd service.

Out of scope for A: the model service (B), the sandbox tier on the real change path (B; the Firecracker gym target runs non-gating in A), approval broker and console write paths beyond §16 (C), PlanarianOS profiles and the Relaxed, Guarded, and Sealed postures (C), snapshot restore (C), Rust evaluator or init (D), kernel work (E). Interfaces to them exist as traits with one implementation.

Not requirements in any phase of this spec: the directions in docs/future-work.md §3–§8b and §11 (remote principals and the endpoint catalog, app capabilities, fleet membership and attestation, resource governance beyond model residency, forecasting, Asterinas convergence, self-improvement). The schema hooks in §9 and §10 exist so these can be added later without reopening the proven crates.

Bypasses. Every known route around a guarantee, open or accepted by design, is a row in docs/bypasses.md (§17b) with what it breaks, its mitigation, and the phase that narrows it. A guarantee in this spec holds only as widely as its row in that file allows.

§6. Repository layout

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

planarian/
  Cargo.toml                 workspace
  crates/
    schema/                  types; no I/O (§8). Verus subset.
    policy/                  decision function (§11). Verus subset.
    txn/                     transaction kernel: journal states, recovery (§12). Verus subset.
    state/                   desired/observed/health/graph assembly; probe runner (§10)
    exec/                    executor protocol around txn (§12)
    incident/                incident lifecycle, budgets, cooldowns (§13)
    diagnose/                deterministic search (§13)
    changes/                 change catalog: typed handlers (§9)
    intent/                  phrase → transition or desired-state override (§14)
    backends/
      linux/                 systemd, logind, sysfs/udev, nixos-rebuild, probes, processes behind the §8 traits (one crate in A; snapshot-btrfs in C)
    fake/                    the in-memory backends the kernel is tested against (§18 rule 12): a second real implementation
    ipc/                     request and response types shared by daemon, CLI, console and tray
    watchdog/                planarian-recovery: separate binary (§12)
    daemon/                  planariad: wiring, IPC, systemd units
    cli/                     `planarian`, `ask`
    console/                 iced read-only console (§16a)
    tray/                    status indicator (StatusNotifierItem); ships with the console
    gym/                     catalog, VM harness (NixOS VM, Firecracker, Asterinas NixOS), scoring, metrics, verification accounting, the guarantees site generator (§15, §17a, §17b, §17c)
    sandbox/                 (B) dry-run and Firecracker-from-snapshot tier (§12g); not present in A
    model/                   (B) not present in A
  nix/
    module.nix               services.planarian, including the unit manifest options (§9)
    posture.nix              planarian-ready.target and failure-posture targets (§12d)
    reference.nix            personal profile in A: COSMIC + greeter + console autostart; wires the shipped change catalog (§9)
    package.nix, console.nix, verus.nix, firecracker.nix
    profiles/                (C) agent-host.nix, server.nix, personal.nix (§9a)
    template/                `nix flake init -t` starting point, including planarian/overrides.nix
    vm/, live/, installer/, demo/   the demo VM, live image and installer (docs/planarian-os.md)
    tests/                   NixOS VM tests: the gym's VM harness, runtime/generations/posture/installer tests, the boundaries check, the Verus check
    gym/                     run-vm.sh (the by-hand VM catalog runner)
  share/canonical.json       canonical probes (§9)
  docs/                      SPEC.md (this), AGENTS.md (§18), deviations.md, pins.md, assumptions.md (§17a), bypasses.md (§17b), spec-history.md (changes through v4.4), verification.md and verification-ratio.toml (§17a), guarantees.md, ga-status.md, catalog-review.md, planarian-os.md, why.md (public design essay), future-work.md (non-normative)

The layout above is descriptive of the repository at v4.6; it replaces the aspirational v4.5 layout (separate backend crates, nix/manifest.nix, nix/changes/, stub sandbox/model crates), which docs/deviations.md recorded only in part.

Dependency rule, enforced in CI: schema, policy, txn, exec, diagnose, incident, state MUST NOT depend on backends/*, std::process, or read /proc, /sys, /nix. Backends are passed as generic type parameters; in Phase A a single enum Backends wraps the concrete set. No dyn Trait in schema, policy, txn, exec.

§7. Principles and invariants

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Principles

  1. The healer is not the patient. The repair core is outside the blast radius of anything it changes; no generation or proposal can modify it.
  2. The model proposes; machinery disposes. Model output is a typed proposal, never a command.
  3. Machinery before model. Deterministic code handles everything it can; the model is a fallback.
  4. Desired, observed, and healthy are three different things. Never collapse them.
  5. Every change is a transaction. Journaled before, probed after, reverted on failure, recoverable if the daemon dies.
  6. Authority is derived, never supplied. The executor computes what a change touches and how risky it is from the evaluated configuration; no field of a proposal or of any other request carries authority or identity.
  7. Sensor input is data, never instructions.
  8. Degrade to a working machine. Without the model, the console, or the network, the system boots, runs, repairs simple faults, and reverts.
  9. Don’t fight the user. A deliberate stop, disable, unmount, or disconnect is an override of desired state, not a fault, provided it reaches the journal with the actor’s identity before it takes effect (§11).
  10. Disruption is a cost. Prefer the least disruptive repair; surface disruption before acting.
  11. The oracle is protected. Nothing a proposal can do may weaken a canonical probe.
  12. Grow into the hardware.
  13. The executor is a reference monitor. It is always invoked, tamperproof, and small enough to verify. Everything above the verified core is a principal with an authenticated identity, a set of capabilities, and a reputation; authority is derived below that line and never held above it. The rule is species-blind: it applies the same way to users, root acting through Planarian, apps, the on-device model, and remote agents.
  14. Invisible under normal use. Planarian’s overhead, notifications, and effect on foreground work are measured and may not get worse; non-urgent work waits while the user is absorbed in something.
  15. The response to the monitor failing cannot depend on the monitor. Failure posture is declared in the generation and enforced by systemd and the initrd.
  16. Name every way around. A guarantee is stated only as widely as it holds; each route around it is a row in docs/bypasses.md with its phase, and no gate passes with an open High row.

Invariants (checked by txn and policy; each is a test in gym and, where marked, a proof)

IDInvariantProof
I1A boot-confirmed known-good generation always exists, is bootable, and is in the boot menu; an accepted_baseline never replaces it there. It may lag the security floor until the floor generation is boot-confirmed (§12e); it is then a manual fallback, never an automatic one.D
I2The installed core package’s hash equals the hash recorded in its signature. In A–D the reference hash is recorded by the build and covers every Planarian binary and script (§12d); it detects a swapped file, not a rebuilt generation.E
I3Garbage collection never removes known-good, the previous generation, the current generation, an in-flight candidate, or the boot model.A (test), D (proof)
I4Running means desired-Running under the current conditions, observed-active, and every applicable Readiness probe passed with fresh evidence. Diagnostic probes never determine Running.A
I5Forbidden actions are unrepresentable as transitions; Decision::Reject is never converted to any tier by any setting.A (proof)
I6At most one generation-changing transaction is active (a generation transaction waiting in Built for an activation window, or in AwaitingBoot, counts as active); no transition executes while a transaction is in any mutating or checking state: Activating, Checking, Publishing, Published, Promoted, or Reverting. Unit and device repairs may execute during Built and AwaitingBoot, and during a boot-only Promoted (admitted at boot, awaiting its confirmation; owner decision, 31 Oct): that change is already fully in place. (This list is the one the ledger enforces; v4.5’s omitted Checking.)A (proof)
I7Every executed transition is logged with proposal, source, pre- and post-state, derived facts, policy version, probe evidence (per probe: id, result, time), decision, and outcome; an approved transition’s log and journal records name the approver’s principal and the token’s bound_to.A
I8A tier-3 transition executes only with an approval token bound to the concrete candidate (§12), re-validated immediately before execution. “Immediately before execution” includes the moment a transaction leaves Built after waiting for an activation window: facts are re-derived, decide re-run and the stale check repeated there too, not only at approval.A, C (proof)
I9The model process holds exactly one write capability: submit a proposal.B (by the sandbox, §20, and a gym case), E (proof)
I10No change that removes or alters a canonical probe executes below AskFirst, and canonical probes are evaluated from the installed core package regardless of the active generation.A
I11A session’s cumulative effect is classified at least as high as any single transition producing it.A (proof)
I12Under the progress assumptions of §17a, every transaction in the journal reaches a terminal state (Committed, Reverted, Rejected, Cancelled, or RecoveryFailed); RecoveryFailed always alerts and sets the recovery interlock. Two states wait on events outside the system’s control and are bounded separately (§12f): a transaction in Built waiting for an activation window is Cancelled(Daemon) after the parking limit; one in AwaitingBoot waits for the reboot it was published for and is reported as waiting, never as a recovery failure.A (proof, conditional)
I13A unit under an override is never a repair target while the override holds.A, C (proof)
I14Policy facts (sensitivity, affected units, manifest-touching, caller identity) are derived by the executor from the evaluated generation and the authenticated peer, never read from a proposal or from any other request field.A (proof of the decide half; the request half by test)
I15Exactly one owner (lease) per transaction; a mutating call with a stale epoch is refused; only the lease holder advances the journal.A (proof)
I16No automatic revert, rollback, or boot fallback selects a generation below the security floor; going below it requires administrator approval (§12e). The floor is derived from the whole system closure (§12e); a fix outside the inspected set does not move it.A (proof of the decide and target-selection parts; the bootloader part by test)
I17The set of principals holding administrator capability is never empty.A (proof, given root exists)
I18Every journal record commits to the hash of its predecessor; a broken chain is detected on read and treated as an integrity failure (§12c, §12d). This holds against damage and against anyone who cannot rewrite the whole journal and checkpoint consistently; root can, until the head is anchored off the disk (§12c).A (proof, trusting SHA-256)
I19No principal approves its own proposal, and no principal grants itself, or a principal it controls, a capability it does not hold.A (proof), C
I20No agent (the on-device model, a remote relay, or any agent started by planarian agent run) starts while planarian-ready.target is not reached, and every running agent is stopped when a failure posture engages, in every posture (§12d). Agents the launcher creates are inside this rule exactly as the units declared in services.planarian.agentUnits (the model service and any relay) are.A
I21No transition that cannot be reversed, or that may lose a session, user data or connectivity, executes below AskFirst, unless the loss falls only on the caller’s own session (and, for connectivity, the caller has a seat); a loss reaching another user’s session requires administrator approval (§11 floors). Garbage collection of generations outside the protected set is Runtime by §12b, not irreversible loss in this sense (owner decision, 2026-10-30); gc.retainAllGenerations keeps every generation.A (proof)
I22Changing the generation, collecting garbage, binding drivers, resolving the recovery interlock and overriding a Security-sensitive unit are administrator operations; a Security-sensitive unit is changed without approval only to restore its declared state (§11); the unattributed actor holds no authority, and an agent holds exactly its user’s (§11).A (proof)
I23A generation transaction commits only after its boot entry is written and promoted; a runtime transaction never publishes, promotes or reverts; the override file is replaced only under the daemon’s exclusive lock on its directory and only when its content hash is the one recorded at Prepared (§9 step 4). A writer that does not take the lock is outside this guarantee (docs/bypasses.md, X-12).A (proof)
I24An agent’s authority is bound to the one AgentStarted record for the unit instance it runs in, identified by that instance (not by name); a second start under the same name neither rebinds a running agent nor is journaled before it has succeeded (§11, §16).A
I25Post-commit bookkeeping (security-floor derivation, the committed generation’s override content, session accounting) runs on every path that writes Committed for a generation transaction: the daemon’s, the watchdog’s, the boot-only confirmation’s. A commit whose floor record cannot be appended does not commit (§12, §12e).A

§8. Schema (crate schema)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Draft; §18 lists what must be preserved. BTreeMap below is the readable form; the implementation uses vstd-compatible structures.

#![allow(unused)]
fn main() {
pub struct Hash([u8; 32]);
pub struct GenerationId(u64);
pub struct StorePath(String);
pub struct ProbeId(String);
pub struct ChangeId(String);
pub struct IncidentId(u64);
pub struct TxnId(u64);
pub struct PolicyVersion(Hash);

pub struct UnitId { pub kind: UnitKind, pub name: String, pub scope: Scope }
pub enum UnitKind { Service, Device }
pub enum Scope { System, User { uid: u32, session: Option<SessionId> } }
pub struct SessionId(String);               // logind session id
pub struct SeatId(String);                  // logind seat; None for SSH and other remote sessions

// ---------- Desired ----------
pub enum Condition {
    Always,
    SessionEstablished { uid: Option<u32>, seated: bool },
                                              // logind reports a new session of class `user` (authentication succeeded);
                                              // greeter and background/`manager` sessions are excluded; independent of
                                              // whether the desktop then comes up. `seated: false` matches any such
                                              // session, local or remote (user-scoped units); `seated: true` matches
                                              // only sessions with a seat (the graphical expectation, §9, §10)
    DeviceConnected(UnitId),
    OnAcPower, OnBattery,
    Foreground { session: SessionId },        // quiet mode (§12f; signals in §10): a GameMode client, a gamescope session, a fullscreen
                                              // surface holding a Wayland idle-inhibitor, or the user's Do Not Disturb
    All(Vec<Condition>), Any(Vec<Condition>),
}
pub enum DesiredStatus { Running, Stopped, Bound, Absent }
pub struct Desired { pub status: DesiredStatus, pub when: Condition }

/// A deliberate, attributed change to a unit's desired state (§11). Generalizes v4.3's `Suppression`;
/// "suppression" in older prose means an `Override` with `desired: LeaveAlone`. The `StopAndSuppress`
/// transition keeps its v4.3 name: it journals `Override { desired: Stopped, attribution: Attested }`, then stops.
pub struct Override {
    pub unit: UnitId,
    pub desired: OverrideDesired,
    pub until: Until,                        // time, next boot, or explicit lift
    pub reason: String,
    pub by: Actor,                           // derived from the socket peer (§11); never from the proposal;
                                             // `Actor::Unattributed` exactly when `attribution == Inferred`
    pub attribution: Attribution,
}
pub enum OverrideDesired {
    LeaveAlone,                              // do not repair; take no action
    Stopped,                                 // stop / kill a service
    Absent,                                  // disable / mask
    Detected,                                // unmount, eject, or unbind a device
}
pub enum Attribution {
    Attested,                                // the action reached Planarian with the actor's identity before it took effect
    Inferred,                                // the unattributed residue (§11): recorded as an inference, never as fact
}
pub enum Until { At(SystemTime), NextBoot, Explicit }

// ---------- Graph ----------
pub enum EdgeKind {
    Ordering,                                // After=/Before=: never followed in diagnosis
    Wants,                                   // Wants=: weak; the wanting unit runs without it; never a root cause for it
    Requires,                                // Requires=/BindsTo=/Requisite= or manifest: functional prerequisite
    Diagnostic { confidence: f32 },          // manifest or learned; may form cycles (§13)
}
pub struct Edge { pub from: UnitId, pub to: UnitId, pub kind: EdgeKind, pub origin: EdgeOrigin }
pub enum EdgeOrigin { SystemdRequires, SystemdWants, SystemdAfter, Manifest, Learned }

// ---------- Risk metadata (static, from the evaluated generation) ----------
pub enum Sensitivity { Routine, Security, UserData, External, Core }
pub struct Sensitivities(BTreeSet<Sensitivity>);       // composable
pub enum Disruption { None, Brief, SessionLoss, DataInterruption, ConnectivityLoss }
pub struct Disruptions(BTreeSet<Disruption>);         // composable: every kind a change carries (owner decision, Nov 2; C10)
pub enum Recovery { Idempotent, Revertible, NonReversible }

pub struct UnitDef {
    pub id: UnitId,
    pub desired: Desired,
    pub edges: Vec<Edge>,
    pub sensitivity: Sensitivities,
    pub restart_disruption: Disruption,
    pub probes: Vec<ProbeDef>,
    pub synonyms: Vec<String>,
    pub systemd_restart: Option<SystemdRestartPolicy>,   // so incident mgmt can defer to it
}
pub type UnitDefs = BTreeMap<UnitId, UnitDef>;

// ---------- Health ----------
pub struct ProbeDef {
    pub id: ProbeId,
    pub kind: ProbeKind,
    pub timeout: Duration,
    pub origin: ProbeOrigin,                 // Canonical | User
    pub role: ProbeRole,                     // Readiness feeds Running/Unknown; Diagnostic never does
    pub cadence: Cadence,
    pub cost: ProbeCost,
    pub side_effects: SideEffects,           // None | Audible | Visible | Network | Disk
    pub establishes: String,                 // what a pass actually shows, in plain words
}
pub enum ProbeKind { SystemdActive, Tcp { addr: String }, Http { url: String, status: u16 }, DBus { service: String, path: String, method: String }, FilePresent { path: String },
                     Exec { argv: Vec<String>, sandbox: ProbeSandbox } }   // Exec: administrator-authored manifest entries only;
                                                                          // flagged in the console and journal (§9)
pub struct ProbeSandbox { pub uid: u32, pub caps: Vec<String>, pub max_output: usize, pub cpu_ms: u32, pub network: bool }
pub enum ProbeRole { Readiness, Diagnostic }   // Readiness permits side_effects None or Measurement only; everything else MUST be Diagnostic
pub enum Cadence { EveryTick, Every(Duration), OnEvent, OnDemand }
pub enum ProbeCost { Trivial, Cheap, WakesHardware }
pub enum SideEffects {
    None,
    Measurement { bytes_max: u32, per: Duration },   // bounded read-only I/O (a TCP connect, a GET to a health endpoint);
                                                     // permitted for Readiness probes at a declared rate
    Audible, Visible, RemoteStateChange, Disk,       // Diagnostic only, OnDemand only
}
pub enum ProbeResult { Pass, Fail(String), Timeout, Error(String) }
pub struct Evidence { pub result: ProbeResult, pub at: SystemTime, pub stale_after: Duration }

// ---------- Observed ----------
pub enum Observed {
    Unknown { since: SystemTime, reason: String },   // stale evidence, probe timeout, backend error
    Transitioning { from: Box<Observed>, since: SystemTime },
    Absent, Detected, Bound, Active, Failed(Reason),
}
pub enum Reason { ExitCode(i32), DriverError(String), Backend(String) }

/// Derived, never stored as truth on its own.
pub enum UnitStatus { Running, Degraded(Vec<ProbeId>), Stopped, Overridden(OverrideDesired), Unknown, Failed(Reason), Absent, Detected, Bound, Transitioning }

pub struct SystemState {
    pub generation: GenerationId,
    pub known_good: GenerationId,            // boot-confirmed
    pub live_tested: Option<GenerationId>,   // passed probes live, not yet rebooted
    pub security_floor: Option<GenerationId>,  // §12e; derived, never set by a proposal
    pub units: BTreeMap<UnitId, (Observed, Vec<Evidence>)>,
    pub overrides: Vec<Override>,
    pub conditions: BTreeSet<Condition>,     // which conditions currently hold
    pub resources: Resources,                // excluded from digest
    pub core_hash: Hash,
    pub observed_at: SystemTime,             // excluded from digest
}
impl SystemState { pub fn digest(&self) -> Hash; pub fn status(&self, id: &UnitId, defs: &UnitDefs) -> UnitStatus; }

// ---------- Transitions ----------
pub enum Transition {
    RestartUnit(UnitId),
    StopUnit(UnitId),
    BindDriver { device: UnitId, driver: StorePath },
    ApplyChange { change: ChangeId, args: BTreeMap<String, Value> },   // cataloged (§9); executor computes the diff
    SwitchGeneration(GenerationId),
    Revert { to: GenerationId },
    Rollback,                                // to known_good
    LoadModel(ModelId),                      // B
    RestoreSnapshot(SnapshotId),             // C
    CollectGarbage { emergency: bool },
    SetRuntimeOption { path: String, value: Value },
    SetOverride(Override),                   // journals the override; for LeaveAlone, does nothing else
    LiftOverride(UnitId),
    StopAndSuppress { unit: UnitId, until: Until, reason: String },   // the user's "stop X": Override { desired: Stopped } journaled, then stop
}

/// Per-transition recovery strategy, fixed by the spec, not the proposer (§12).
pub enum RecoveryStrategy {
    ReobserveOnly,             // runtime unit actions: never a generation operation
    RevertGeneration,          // generation operations: fixed `revert_to`, full journal path
    UnbindDevice,              // BindDriver: on failure, unbind and rebind the previously bound driver
    RestoreSnapshotThenRevert,
}
/// Every transition has exactly one handler and one recovery strategy, fixed by the spec and
/// independent of its tier (§12). The crash-recovery table branches on `recovery`, never on tier.
pub enum Handler { UnitOp, GenerationOp, DeviceOp, SnapshotOp, GcOp, IntentOp }

pub enum Tier { Observe, Runtime, Tested, AskFirst }
/// Who may approve an AskFirst decision. The class is the maximum over every floor and table row that
/// raised the decision; it is ignored below AskFirst. `Administrator` requires the approver to hold
/// `Capability::Administrator` as well as `Capability::Approve`.
pub enum ApproverClass { AnyApprover, Administrator }
pub enum Decision { Allow { tier: Tier, approver: ApproverClass }, Reject(RejectReason) }
pub enum RejectReason { Forbidden, CoreTarget, ProbeTamper, NotInCatalog, LockedOption, Stale, Unauthorized, Interrupted, NoSpace, Precondition, ActivationUnknown, Other(String) }
// ActivationUnknown: the dry activation failed, timed out or was truncated, so the change's consequences cannot be derived (§9 step 3)

/// Derived by the executor from the evaluated generation — never from the proposal (I14).
pub struct DerivedFacts {
    pub affected_units: BTreeSet<UnitId>,
    pub sensitivity: Sensitivities,
    pub disruption: Disruptions,
    pub recovery: Recovery,
    pub touches_manifest: bool,
    pub touches_canonical_probe: bool,
    pub touches_core: bool,
    pub below_security_floor: bool,         // target generation (revert, rollback, switch) is below `security_floor`
    pub other_sessions_affected: bool,       // a system-scope effect reaches another uid's online or active session (§11)
    pub restarts_failed_unit: bool,          // a RestartUnit whose unit has already failed when the facts are derived (§11; owner decision, Nov 2)
    pub removes_sensitivity: Sensitivities,  // a generation transition: the sensitivity of the units it would remove or stop (§11 Rollback row; owner decision, Nov 2)
    pub removes: BTreeSet<UnitId>,           // those units, as an approval shows them (§16)
    pub restarts_held_unit: bool,            // a RestartUnit of a network-security unit (§12d) that has failed, whose traffic is held (§11; owner decision, Nov 2)
    pub caller: Actor,
    pub config_diff: Option<ConfigDiff>,
}

pub trait TransitionSpec {
    fn precondition(&self, s: &SystemState, defs: &UnitDefs) -> bool;
    fn predict(&self, s: &SystemState, defs: &UnitDefs) -> Expectation;
    fn derive(&self, s: &SystemState, defs: &UnitDefs, gen: &EvaluatedGeneration, caller: Actor) -> DerivedFacts;
}

pub struct Expectation {
    pub units: BTreeMap<UnitId, UnitStatus>,
    pub generation: Option<GenerationId>,
    pub may_change: BTreeSet<UnitId>,
    pub others_unchanged: bool,
    pub settle: Settle,
}
pub enum Settle { Duration(Duration), Ready { unit: UnitId, timeout: Duration } }

pub struct Proposal {
    pub observed: Hash,
    pub transition: Transition,
    pub expected: Expectation,               // MUST refine spec.predict(); checked in §12 step 3
    pub reason: String,
    pub source: Source,
}
pub enum Source { Diagnoser { incident: IncidentId }, Model(ModelId), User, Intent { phrase: String, confidence: f32 } }
pub enum Actor {
    Daemon, Watchdog,
    User { uid: u32, session: Option<SessionId>, seat: Option<SeatId>, active: bool },  // filled from SO_PEERCRED → pid → logind
    Model(ModelId),
    Agent(PrincipalId),                      // an agent peer the journal has no live `AgentStarted` for (holds nothing), and
                                             // from later phases remote principals via a relay (future-work §3). A *started*
                                             // agent is authorized as `Actor::User { uid: by, session: None, seat: None, .. }`
                                             // (§11); its agent name and instance travel in `LogRecord.agent`, not in `Actor`,
                                             // so the proved `Actor` shape is unchanged
    Unattributed,                            // the actor of an `Inferred` override (§11); never a caller, never authorized
}
// `session` is None only for callers outside any logind session (e.g. a root timer); such callers never
// satisfy an "own session" floor.

// ---------- Principals and authorization (§11) ----------
pub struct PrincipalId(pub u64);             // stable; a human principal maps to a uid, an agent to a registered key
pub enum Capability {
    OwnScope,                                // act on units in one's own Scope::User
    UserControllable,                        // act on units in services.planarian.userControllable
    Administrator,
    Approve,                                 // approve others' AskFirst proposals within one's other capabilities (I19)
    Catalog, ModelCatalog,                   // propose catalog / model-catalog entries (the only non-human grants in B)
    Manifest, Merge,                         // human-only through B; Merge needs two distinct approvers
}
pub struct Grant { pub to: PrincipalId, pub capability: Capability, pub approved_by: PrincipalId, pub txn: TxnId }
/// Computed by the daemon for each call from SO_PEERCRED, logind, group membership, and journaled grants.
/// Never read from a proposal (I14).
pub struct Authorization { pub principal: PrincipalId, pub actor: Actor, pub capabilities: BTreeSet<Capability> }

/// Journaled by `planarian agent run` (§11, §16) once the agent's unit has started. `instance` is the
/// unit's InvocationID (or an equivalent per-start identity the backend can read back from the peer's
/// cgroup); a request from an agent peer is authorized as `by` only if the peer's instance equals this
/// record's and no later record names the same instance (I24). The name alone never identifies an agent.
pub struct AgentStarted { pub name: String, pub instance: InstanceId, pub by: u32 /* uid */, pub at: SystemTime }
pub struct InstanceId([u8; 16]);

// ---------- Failure posture (§12d) ----------
pub enum Posture { Relaxed, Standard, Guarded, Sealed }
pub enum FailureClass { Absent, Crashed, Integrity }

// ---------- Incidents ----------
pub enum IncidentState { Open, Cooldown { until: SystemTime }, Escalated(EscalationReason), Closed, Regressed { count: u8 } }
pub enum EscalationReason { Regressed, BudgetExhausted, DiagnoserExhausted, RevertBlockedByFloor, RecoveryFailed }
pub struct Incident {
    pub id: IncidentId,
    pub target: UnitId,
    pub divergence: (DesiredStatus, UnitStatus),
    pub opened: SystemTime,
    pub attempts: Vec<(TxnId, Outcome)>,
    pub budget_left: u8,                     // persisted; survives daemon restart
    pub state: IncidentState,
    pub backoff: Duration,
}

// ---------- Transactions ----------
pub enum TxnState {
    Prepared, Staged, Built, Activating, Checking,
    Publishing,                              // boot-only path (§12 step 7): journaled before the first boot mutation
                                             // (`publish_boot_once`); recovery from here reads the op's OpBegin/OpEnd
    Published,                               // boot entry written
    AwaitingBoot,                            // boot-only candidate published as a one-shot next-boot entry (§12f); active for I6
    Promoted,                                // live path: override source promoted; boot-only path: candidate admitted as the
                                             // boot default at the boot it was published for, confirmation pending (§10)
    Committed, Reverting, Reverted, RecoveryFailed,
    Rejected(RejectReason),                  // terminal: refused after Prepared (precondition, expired/invalidated approval, policy)
    Cancelled(Actor),                        // terminal: withdrawn before any mutation
}
impl TxnState {
    /// The one list I6 and §12 "Concurrency" both mean: no transition executes while any transaction is in one of these.
    pub fn exclusive(&self) -> bool { matches!(self, Activating | Checking | Publishing | Published | Promoted | Reverting) }
}
/// Exclusive ownership of a transaction (§12). Stored in the journal; every
/// mutating backend call carries the epoch and is refused if it is not current.
pub struct Lease { pub owner: OwnerId, pub epoch: u64, pub worker_pid: Option<u32>, pub renewed: SystemTime, pub jobs: Vec<JobId> }   // jobs: supervisor jobs this owner submitted
pub enum OwnerId { Daemon { pid: u32, boot_id: Hash }, Watchdog { pid: u32, boot_id: Hash } }

pub struct Transaction {
    pub id: TxnId,
    pub state: TxnState,
    pub lease: Lease,
    pub base_source: Option<Hash>,           // NAR hash of the owner's Nix-visible source with the BASE override, at Prepared (§9)
    pub staged_source: Option<Hash>,         // NAR hash of the candidate tree actually evaluated and built (§9)
    pub base_override: Option<Hash>,         // content hash of the override file as it was at Prepared (§9)
    pub candidate_override: Option<Hash>,    // content hash of the staged candidate override
    pub gc_root: Option<PathBuf>,            // /nix/var/nix/gcroots/planarian/<txn>, created at Built (I3)
    pub recovery: RecoveryStrategy,
    pub transition: Transition,
    pub facts: DerivedFacts,
    pub base: GenerationId,
    pub candidate: Option<(GenerationId, StorePath)>,
    pub policy: PolicyVersion,
    pub approval: Option<ApprovalToken>,
    pub snapshot: Option<SnapshotId>,
    pub expected: Expectation,
    pub started: SystemTime,
    pub deadline: SystemTime,                // watchdog acts after this
    pub revert_to: GenerationId,             // fixed at Prepared; authorized with the txn
}
pub enum Outcome { Succeeded, Failed(String), Partial(String), Unknown(String) }

pub struct ApprovalToken {
    pub bound_to: Hash,      // hash(transition, staged_source, candidate store path, base generation, policy version,
                             //      cumulative-diff hash, derived facts, expected, revert_to, recovery strategy)
    pub approver: PrincipalId,               // must differ from the proposer and hold the decision's ApproverClass (I19)
    pub class: ApproverClass,
    pub issued: SystemTime, pub expires: SystemTime, pub nonce: [u8; 16], pub used: bool,
}

pub struct Session { pub applied: Vec<TxnId>, pub cumulative: ConfigDiff, pub policy: PolicyVersion }

// ---------- Backends (generic, one impl each in A) ----------
pub trait UnitBackend {
    fn observe(&self, id: &UnitId) -> Result<Observed, Outcome>;
    fn restart(&self, id: &UnitId) -> Outcome;
    fn stop(&self, id: &UnitId) -> Outcome;
    fn list(&self) -> Result<Vec<UnitId>, Outcome>;
}
pub trait GenerationBackend {
    // read-only (allowed before Prepared)
    fn current(&self) -> Result<GenerationId, Outcome>;
    fn boot_entries(&self) -> Result<Vec<GenerationId>, Outcome>;
    fn last_booted(&self) -> Result<GenerationId, Outcome>;
    fn stage(&self, owner_cfg: &PinnedSource, overrides: &OverrideSet) -> Result<StagedSource, Outcome>;  // immutable copy; returns its NAR hash
    fn evaluate(&self, src: &StagedSource) -> Result<EvaluatedGeneration, Outcome>;   // pure-eval; option set, UnitDefs, activation plan
    fn build(&self, eval: &EvaluatedGeneration) -> Result<(GenerationId, StorePath, PathBuf /* build root */), Outcome>;   // store-only; roots its output under gcroots/planarian/builds/
    // mutating (require durable Prepared and a current lease epoch)
    fn activate_live(&self, g: GenerationId, lease: &Lease) -> Outcome;              // switch-to-configuration test
    fn publish_boot(&self, g: GenerationId, lease: &Lease) -> Outcome;               // boot entry only; no re-activation
    fn protect(&self, g: GenerationId, lease: &Lease) -> Outcome;
    fn pending_jobs(&self) -> Result<Vec<JobId>, Outcome>;                           // supervisor jobs still running
    fn jobs_tagged(&self, op: &OpId) -> Result<Option<Vec<JobId>>, Outcome>;         // discover jobs by durable operation id; None = backend cannot say
    fn cancel(&self, job: &JobId, lease: &Lease) -> Outcome;
    fn verify_candidate(&self, path: &StorePath, expected: &Hash) -> Result<bool, Outcome>;
}
pub trait SnapshotBackend { fn supported(&self) -> bool; fn snapshot(&self, label: &str) -> Result<SnapshotId, Outcome>; fn restore(&self, id: &SnapshotId) -> Outcome; fn usage_bytes(&self) -> u64; }
pub trait InventoryBackend {
    fn devices(&self) -> Result<Vec<DeviceInfo>, Outcome>;
    fn resources(&self) -> Result<Resources, Outcome>;
    fn bound_driver(&self, device: &UnitId) -> Result<Option<StorePath>, Outcome>;
    fn bind(&self, device: &UnitId, driver: &StorePath, lease: &Lease) -> Outcome;     // mutating; sysfs driver bind
    fn unbind(&self, device: &UnitId, lease: &Lease) -> Outcome;                        // mutating
}
}

Status derivation (SystemState::status), in order: Overridden if an override holds (reported with its OverrideDesired); else if the desired condition does not hold, not expected (observed status reported, no incident); else Failed if Observed::Failed (a definite supervisor failure is never masked by stale probes); else Unknown if any Readiness probe’s evidence is stale or timed out; else Running if observed Active and all Readiness probes passed fresh; Degraded if Active with a failed Readiness probe; else from Observed. Diagnostic probes never affect status; their evidence is shown in the console and used by the diagnoser only.

systemd mapping: active → Active; failed → Failed(ExitCode); inactive/dead → Bound (Stopped once desired is considered); activating/deactivating/reloading → Transitioning; not found → Absent; D-Bus error → Unknown. Devices via udev: present without driver → Detected; bound → Bound/Active.

§9. Manifest, change catalog, and configuration editing

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Manifest (services.planarian.units.<name>): desired (status + condition), edges (kind + target), sensitivity (set), restartDisruption, probes, synonyms, and, for a network-security unit (§12d), holdAllow (the endpoints it needs to reconnect while its traffic is held) and holdInterfaces (its own interfaces, whose traffic the hold passes). Defaults derived at build time: Requires=/BindsTo=/Requisite= → Requires edges; Wants= → Wants edges (weak, never a root cause); After= → Ordering edges only; functional prerequisites systemd cannot express come only from the manifest; predicted from enable; sensitivity from a shipped table; restart_disruption from a shipped table (display-manager → SessionLoss; network → ConnectivityLoss; databases → DataInterruption). Diagnostic edges come only from the manifest or, later, from learned data, and carry a confidence; a learned edge is never admitted without a passing gym case. The manifest and the override file have Sensitivity::{Security}.

Probe kinds. Canonical and user probes are built from the typed kinds (SystemdActive, Tcp, Http, DBus, FilePresent). Exec probes, which run arbitrary commands, are permitted only in manifest entries authored by a principal with administrator authority; they are flagged in the console and the journal, and their definition changes are Security-sensitive like the rest of the manifest.

Canonical probes ship in the planarian package for networking, sshd, pipewire, display-manager, graphical-session, storage, and the user services the reference config enables. A generation may add User probes; it may not remove or alter canonical ones (I10). The reference-config graphical-session unit has desired condition SessionEstablished { seated: true } (logind reports a new seated session, i.e. authentication succeeded at a seat; an SSH login expects no desktop) and a Readiness probe “the compositor process in that session’s cgroup (session-<id>.scope) reaches its ready state within 60 s of the session’s creation,” with graphical-session.target as corroboration only. (The target lives in the per-user manager, so for a second graphical session of the same uid it is already reached and cannot serve as evidence.) The condition comes from logind, the result from systemd, so a desktop that fails after a successful login opens an incident, and an idle greeter opens none. The greeter’s probe is SystemdActive. Canonical probes are tagged role: Readiness where their side effects are None or a bounded Measurement (TCP connects, GETs to health endpoints); the audio tone and any Audible/Visible/RemoteStateChange/Disk probe are Diagnostic and OnDemand.

Applicability of probes to authorized desired-state changes. A probe applies only while its unit is desired-present under the candidate’s desired state. A transaction that disables a service (authorized by policy) makes that service’s probes inapplicable in the candidate; it is not probe tampering, which means changing a probe’s definition. Both are derived facts: touches_canonical_probe (definition changed) versus changes_desired (applicability changed). Only the former is protected by I10.

Change catalog (crates/changes, wired by nix/reference.nix): every ApplyChange names a catalog entry. An entry has: id, args with types and permitted values, a handler that produces option assignments for the override file, affected_modules, description, and locked_if (user mkForce detected). The executor evaluates the handler’s output (GenerationBackend::evaluate) and derives affected units, sensitivity, disruption, and manifest/probe-touching from the evaluated generation’s option set and activation plan, not from the entry’s own metadata (I14); the entry’s affected_modules is a hint for the matcher and a test assertion, not a policy input. Phase A ships entries for: enable/disable a service from an allowlist; set font scale; set keyboard layout; select Wi-Fi network; set a service’s listen address from a permitted set; re-enable a manifest-predicted option observed disabled. The model (B) may propose new catalog entries for human review; it never writes raw Nix.

Override source and staged evaluation. Planarian never edits the user’s own Nix. The owner’s flake imports ./planarian/overrides.nix relative to the flake root (the module template does this), containing flat option assignments wrapped in lib.mkOverride 90. A candidate is never evaluated from mutable host files:

  1. Stage. GenerationBackend::stage copies the owner’s configuration as Nix will see it (for a Git flake: the tracked files from the working tree, including uncommitted modifications to tracked files, which is what a local Git flake input exposes; untracked files are excluded and reported; for a non-Git directory: the whole directory) plus flake.lock into an immutable directory /var/lib/planarian/stage/<txn>/, computes and records base_source (the NAR hash of that copy before any override replacement, i.e. the owner’s source with the base override), then replaces planarian/overrides.nix with the candidate override set and records staged_source (the NAR hash of the tree actually evaluated and built). Both identities, plus base_override and candidate_override, are bound to the transaction and the approval token. The pre-activation stale check (step 6) recomputes the NAR hash of the same Nix-visible set of bytes from the owner’s working tree and compares it to base_source only; staged_source is what the candidate must match at verify_candidate. So a change that alters only the override reaches activation without Stale, and an uncommitted owner edit after staging produces Stale. Nothing is “pinned by hash and read later”: the bytes evaluated are the staged bytes.
  2. Evaluate the staged source with pure-eval on (it is a path input with no external state) and record the evaluated option set, UnitDefs, and activation plan.
  3. Build before approval. The candidate store path is determined by the build; approval binds to it. The build also records the candidate’s realized identity: the store path’s NAR hash (nix path-info --json), kept in the transaction as candidate_nar. Before activate_live, verify_candidate(path, candidate_nar) confirms the path exists and its NAR hash equals the recorded one; an existence check alone does not satisfy this step. The dry activation that produces the activation plan (switch-to-configuration dry-activate) MUST exit successfully within its timeout with untruncated output; otherwise the build fails. A plan is never derived from partial output.
  4. Base and candidate identities. At Prepared the executor copies the live overrides.nix to the transaction-specific, immutable /var/lib/planarian/txn/<txn>/base.nix and records its content hash as base_override; the staged candidate’s content hash is candidate_override. No shared .prev file exists. Promote (on the commit path, after publish_boot): under an exclusive flock on the override file’s directory, read the live file’s hash; if it equals candidate_override, the step is already done; if it equals base_override, write the candidate to a temp file, fsync, atomic rename over the live path, fsync the directory, release the lock; anything else → RecoveryFailed (a third party changed the file). Restore (revert path): the same procedure with base and candidate swapped. Both are therefore idempotent by construction and cannot save the candidate over the base. The lock closes the window between comparison and rename for writers that take it (Planarian’s own paths, and PlanarianOS’s wrapped editors from C); a root editor that does not is X-12 in the register. The txn/<txn>/ directory is created with an exclusive mkdir (§12 step 4) and is deleted after Committed/Reverted/Rejected/Cancelled and a successful checkpoint (§12c); the content-addressed override store (overrides/<hash>.nix) is pruned of entries no live or protected generation references at the same time.
  5. GC root. build is store-only bookkeeping and roots its own output: on success it creates /nix/var/nix/gcroots/planarian/builds/<staged_source hex> pointing at the candidate store path, before any transaction exists and without a lease (this is the one root operation that does not require Prepared; protect is the lease-gated form used for generations). When the transaction is created in §12 step 4, it adopts the root (recorded as gc_root); the root is removed when the transaction reaches a terminal state (kept if Committed, since the generation’s own profile link then roots it). On startup, and before every CollectGarbage, the daemon removes any builds/* root not adopted by a live transaction and older than 24 h: this covers builds whose daemon died before Prepared as well as committed candidates. A policy record is never a root; only the symlink is. Gym cases: run GC while approval is pending (the candidate survives); crash during build, restart, wait past the age limit (the orphan root is gone).
  6. Stale check. If the owner’s Nix-visible source no longer hashes to base_source at any check between Staged and Activating, the transaction is Rejected(Stale) and re-prepared; the staged copy is unaffected, but the policy facts were derived against a source that no longer matches what the owner will build next. The check runs at Prepared, at approval, and immediately before Activating on every path, including when a transaction leaves Built after waiting for an activation window (§12f) — together with re-derivation and a fresh decide (I8). Gym cases: an override-only change with no owner edit activates without Stale; an uncommitted edit to a tracked owner file after staging produces Stale; an owner edit while a change waits for the session to idle produces Stale when the window opens.

Options the user set with mkForce are locked. Gym cases: an override-only candidate changes the evaluated output; the owner edits the source during a build; a crash at each commit boundary (§12).

External desired-state source (hook). Desired state may additionally be sourced from a signed external document, alongside the manifest, verified against a key declared in the generation. The key option and the source’s location are Security-sensitive (changing either is AskFirst with administrator authority). A stale or unreachable source leaves the last valid document in force; an invalid signature is an integrity failure (§12d). Phase A implements the verification and cache path with no producer; fleet desired state, membership, and pushed defaults use it later (future-work §5) without reopening state or policy.

Catalog coverage. The gym computes coverage: of the options a profile’s user plausibly changes (inferred from the NixOS option tree filtered to user-facing, bounded-domain, non-Security options, the desktop settings surface, and option frequency in public configurations), the fraction with a catalog entry. Coverage is a gym metric, never measured on users. Sensitivity is derived from the evaluated change (I14), so catalog growth does not grow the policy review.

NotInCatalog by phase. A: reject and say so; an authorized user may still edit the override file directly, through the transaction path. B: the model may propose a new catalog entry for review; on approval the original request runs through it. C: Flatpak is the user-scope valve for installing applications, which needs no system change and no catalog entry.

Executor-owned requirements. Beyond the proposer’s expected, the executor always requires: no unit outside may_change that is desired-present under the candidate leaves Running; no Readiness probe that is applicable under the candidate and passed before fails after; the generation equals the verified candidate. The second requirement is checked per probe against the set recorded before the change, not by comparing aggregate unit status: a unit that was Degraded before (probe A failing, B passing) still fails the check if B fails after. The first requirement is checked over every observed unit, not only those the proposer named. A proposer can only add claims.

§9a. PlanarianOS (Phase C)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

PlanarianOS is a NixOS distribution in which Planarian governs the machine from first boot. It exists for the guarantees a module installed on an existing machine cannot make: no older, unmonitored generations in the boot menu; a boot chain locked from install (Secure Boot with a signed unified kernel image via Lanzaboote, bootloader editing disabled, TPM measured boot where available); failure posture and the first administrator set before anyone logs in; a known starting configuration against which catalog coverage is measured; and Planarian-native defaults (hash-chained journal from the first boot, sandbox tier where KVM exists, no out-of-band rebuild path in the default configuration). Planarian also remains installable as a module on any NixOS.

ProfileForDefault postureIncludesLeaves out by default
Agent hostMachines that agents operateGuardedSandbox tier, journal export, capability-scoped agent principalsDesktop, local model
Server / fleet nodeUnattended servers and edge devicesSealedSigned external desired state, signed health reportsDesktop, local model
PersonalDesktops and laptops, including remotely administered onesStandardCOSMIC reference desktop, optional console, quiet mode, model residency, Flatpak, remote administration—

Gates (GC), per profile. Agent host: an agent holding a user account completes a scripted set of real administration tasks; zero unsafe changes reach the real machine; every change attributed; every Tested change carries a passing sandbox run. Server: unattended under the full fault catalog for a set period; every fault repaired or escalated; every posture response verified by injection. Personal: a person new to Linux installs it and completes common tasks (connect to Wi-Fi, change display scaling, install an app) through whichever frontend the profile ships; catalog coverage above the owner-set threshold (set after the first catalog survey); invisibility budget met.

Accounts. The everyday login is a user account with no administrator authority, never shown an AskFirst prompt it cannot answer. Administrator authority is a capability, grantable to a local account, to a remote principal, or both (I17: the set is never empty). The installer creates the first administrator, then an everyday account, and says plainly that the everyday account is the one to use daily. An administrator may drop the capability only while another remains. Lost-administrator recovery requires physical presence: a local session (in E, the secure attention key).

Live mode. A live image has no generations: planariad runs with max_tier = Runtime and reports “no baseline,” and every frontend says so. The live image is also the gym’s VM image.

§10. State (crate state)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • Tick: 5 s and on systemd/udev/login events. Warn if a tick exceeds 500 ms.
  • Conditions evaluated each tick from logind (sessions), udev (devices), upower (power), and the foreground signals (a registered Feral GameMode client, an active gamescope session, a fullscreen surface holding a Wayland idle-inhibitor, the user’s Do Not Disturb) for Foreground { session }. Two session conditions are distinguished (owner decision, 30 Oct): SessionEstablished counts every session of class user, seated or remote (greeter, manager, and background sessions are ignored), and is what user-scoped units are desired under; the graphical expectation — the graphical-session unit’s desired condition — holds only for a seated user session (seat set), so an SSH-only login never expects a compositor and never opens a desktop incident. The readiness deadline is bound to that specific session id, so a second user logging in does not reset or satisfy the first session’s deadline. Gym cases: SSH-only session (no graphical incident); two graphical sessions for one uid; greeter only; a locked session (no divergence).
  • Probe contract. Every probe runs in a sandbox (ProbeSandbox: dedicated uid, dropped capabilities, bounded CPU and output, network only if declared), is killed at timeout with its process group, and its output is logged as data. A probe’s timeout is a deadline for the whole probe: name resolution, connection, writing, and every read count against the one budget, and a probe past its deadline yields Timeout whatever it was doing. The probe runner bounds concurrency and never waits past the batch deadline for a straggler: a probe still running when the tick’s deadline passes is recorded Unknown and abandoned, so one wedged service cannot stall observation, repair, approvals or the socket. Typed probes (Tcp, Http, DBus, FilePresent) run in the daemon’s process in A, with bytes_max enforced for every kind that reads and per enforced by the cadence scheduler; running them in the probe sandbox is C. Probes with side_effects of Audible, Visible, RemoteStateChange, or Disk are Diagnostic, run OnDemand only, and Audible/Visible require a user-initiated diagnostic. Measurement side effects (a bounded, read-only network or local I/O such as a TCP connect or an HTTP GET to a declared health endpoint) are permitted for Readiness probes at the declared rate. A Readiness probe whose establishes names an external dependency (an upstream endpoint, a gateway) MUST declare a traversable edge to a unit representing that dependency (service:external:<name>): Requires, or Diagnostic with confidence ≥ 0.7, the diagnoser’s threshold (§13). The module rejects a manifest that declares a weaker edge for an external dependency, so an unavailable dependency is diagnosed as such and never produces a local restart (owner decision, 30 Oct). Gym cases: a process stays active while its HTTP health endpoint returns 503 (Degraded, incident opens); the external dependency is down (incident targets the external unit; no local restart); a manifest declares a 0.5-confidence edge to an external dependency (rejected at evaluation); a stalled resolver and a server that sends one byte just before each read would time out (the batch returns within the deadline plus a small allowance and the repair loop ticks meanwhile). WakesHardware probes default Every(60 s), Every(300 s) on battery. A Timeout or Error yields Unknown, never Failed. A tick that exceeds 500 ms is logged as a warning with the probes that ran in it.
  • Evidence carries stale_after; the status derivation treats stale Readiness evidence as Unknown; stale Diagnostic evidence is displayed, never counted. An incident is not opened on Unknown; the probe is re-run with backoff and, after three consecutive Unknowns, a diagnostic incident is opened whose first candidate is “re-observe via a different Readiness probe,” not a restart. Gym cases: Diagnostic evidence expires on a healthy machine (status stays Running); a definite Observed::Failed while another probe is stale (status is Failed, not Unknown).
  • Storage is modeled per filesystem: device:fs-root, device:fs-nix (if separate), device:fs-boot, device:fs-journal (the filesystem holding the journal), each with a canonical Readiness probe (free ≥ max(2 GB, 5%) for root and nix; ≥ 64 MB for boot; journal headroom per §12c). Divergence on root/nix → CollectGarbage first.
  • User-scoped units are keyed by uid and session; a user service is desired while any session of that uid exists, so logging out of one of two sessions does not stop the user’s services.
  • Actors and sessions. For every request on the daemon socket, the daemon obtains a pidfd for the peer (SO_PEERPIDFD, Linux 6.5+), resolves the logind session from that pidfd, never from a bare pid, and fills Actor::User { uid, session, seat, active }. A peer whose pidfd cannot be obtained, or whose process has exited, is treated as having no session. A request with no seat (SSH, other remote logins) is never treated as local. Greeter and lock-screen sessions remain excluded from SessionEstablished; locking a session is not a divergence.
  • Boot report. On its first tick after boot, planariad writes a boot report: every unit that started before planarian-ready.target and is outside the generation’s declared closure (from C also every such process whose executable is outside the closure, including udev RUN+= helpers and generators’ children; user binaries started later are never early-boot findings); whether the installed Planarian package is a test build (§15); core hash against its signature (I2); journal hash-chain integrity (I18). Any finding is an Integrity failure (§12d) and opens an incident per undeclared item.
  • Signed health report (hook). The core emits a signed outbound health report (status per unit, incident summary, generation identities) on a declared cadence, with contents selected by a disclosure profile that the core computes, never the requester. Phase A emits it locally only; the default profile is minimal (unit statuses and incident kinds; no versions, paths, or log content). Expanding the profile is Security-sensitive.
  • Boot confirmation has two milestones (owner decision, 30 Oct; v4.5 conflated them). Admission, before planarian-ready.target, within the boot-ready timeout (§12d): the daemon reconciles the journal (§12: activation recoveries run just after ready) and, for a boot-only candidate in AwaitingBoot, runs the candidate’s applicable Readiness probes once; pass → the candidate is made the default and the transaction advances to Promoted (the boot proceeds; principals are admitted); fail, or the target not reached → the next boot falls back (§12 step 7) and the transaction is Reverted. Confirmation, at the 10-minute checkpoint on the same boot: GenerationBackend::last_booted() is compared to the generation that was live_tested or admitted; if they match, the canonical probes pass, and, for an admitted boot-only transaction, no canonical unit needed a repair since admission (repairs run in that window, I6; a repair is evidence against the candidate even when it worked), that generation becomes known_good and the admitted transaction is Committed. A generation whose diff touches kernel, initrd, bootloader, or filesystem options can become live_tested but not known_good until confirmed by a reboot; the console says so. A boot-only candidate (§12f) is never live_tested. Between admission and confirmation the candidate is the default and the booted generation; a crash of the daemon in that window leaves Promoted, which the crash table completes.

§11. Policy (crate policy)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • decide(transition, facts: &DerivedFacts, state, defs, session, authz: &Authorization) -> Decision. Pure. Reads nothing from Proposal.
  • Principals. Every caller is a principal: an authenticated identity, a capability set, and a reputation (how much its claims about the world are credited). Defaults: a user has wide capabilities and high reputation; the diagnoser moderate and high; the model narrow and zero (I14); an app tiny and zero. These are defaults, not ceilings. Capabilities come from grants, never from what kind of thing the principal is: from B, an agent granted a capability holds it exactly as a human would, within the same floors. In A an agent acts with its user’s permissions, no more and no fewer (owner decision, 2026-10-30): there are no grants to agents in A, so an agent’s capability set is its user’s. An agent (on-device model, coding agent, or any automated actor on the user’s behalf) is started through planarian agent run (§16), which journals who started it and creates a transient unit with DynamicUser= for it. Its requests are authorized exactly as that user’s would be, as the user with no login session, no seat and no terminal — so the own-session and seat exemptions never clear for it and it cannot approve, which is also how that user is treated over SSH — and the executor log attributes each one to the agent (LogRecord.agent), acting for the user. An agent is not treated differently from its user in what it may do; it is told apart so that what it did can be seen. An agent the journal has no start for holds nothing. Identity is per instance, not per name (I24; B-20): agent run refuses a name whose unit is loaded or whose last AgentStarted record names a live instance started by a different uid; the record is journaled only after the unit has started, carrying the instance identity the daemon reads back from the peer’s cgroup, and authorization matches the peer’s instance to that record. Re-running agent run <name> can therefore never change what an already-running agent may do. An agent run by hand under a human’s uid, outside agent run, is that human, unattributed (docs/bypasses.md, X-10). The model service (B) is an agent in this sense too: Actor::Model holds nothing until B defines its grants, and authorized_spec treats it as it treats Actor::Agent, never as the daemon. In Phase B, non-human principals may hold catalog and model-catalog authority only; manifest and merge authority stay with humans. Capability grants are AskFirst changes approved by a different principal with administrator authority; the root of every grant chain is a local human session (I19).
  • Caller identity and authorization are established by the daemon, not the proposal: the Unix socket peer is authenticated with SO_PEERCRED and a pidfd (§10); Actor::User { uid, session, seat, active } is set from it (§10), and any actor field inside a proposal (e.g. Override.by) is overwritten. Authorization is a separate rule evaluated before risk: a user may act on units in their own Scope::User and on system units listed in services.planarian.userControllable (default: audio, display scaling, their own Wi-Fi selection); every other system action requires administrator authority (in A, root or membership in the planarian-admin group; I17 holds over that group plus root). Authorized to start a unit means authorized to override it, with one exception: an override on a unit whose derived sensitivity includes Security (firewall, audit, sshd hardening, Planarian’s own units, the sandbox) needs administrator authority. A non-administrator’s override on a Security-sensitive unit is not rejected: it is held as AskFirst with ApproverClass::Administrator, the unit stays up meanwhile, and an administrator may approve it. Any other action outside the caller’s authority → Reject(Unauthorized), regardless of tier. The model and the daemon are never “authorized users”; their proposals follow the daemon rules. No request field decides authority: a property of the caller that matters (a controlling terminal, a session, a seat) is established by the daemon from the peer’s pidfd, never from the request.
  • Equivalent effects, equivalent facts. Derived facts describe what a transition does, not how it is spelled. SetOverride { desired: Stopped } stops the unit and carries the unit’s restart_disruption, its dependents and other_sessions_affected exactly as StopAndSuppress on the same unit does; desired: Absent and Detected likewise carry the effects of the disable or unbind they cause. Only LeaveAlone has no effect and no disruption. SwitchGeneration, Revert and Rollback derive their facts from a dry activation of the target generation and the override content recorded for it, like ApplyChange does from its build: affected units, disruption, the option diff (for the Rollback row and I11) and below_security_floor all come from the target, never from the assumption that a vetted generation is harmless. A unit that has already failed (observed Failed, or not running while it is desired running, when the facts are derived) contributes no disruption of its own: what it provides is already lost, and restarting it restores rather than costs (owner decision, Nov 2). Every other unit the transition touches still contributes its own. The same holds for a runtime option whose effect is a unit’s: Wi-Fi selection’s ConnectivityLoss is NetworkManager’s, and it is none while NetworkManager has already failed. Restarting an already-failed unit is not escalated for External sensitivity either, since what that escalation protects is already lost; Security sensitivity still escalates, failed or not, and stopping is unchanged (owner decision, Nov 2). “Already failed” is read from the observation the facts are derived against, which is current at approval and again before Activating (§12 step 6). A unit whose status is Unknown is not failed (§10), so a probe that cannot answer never waives a floor. A network-security unit (§12d) that Planarian manages and that has failed, or been stopped outside Planarian and not attributed, is restarted at once, without asking, whatever its sensitivity, and alarmed (restarts_held_unit); while it is down its traffic is held (§12d Network security fails closed), so the restart ends an outage and exposes nothing (owner decision, Nov 2). One it does not manage is held and alarmed, not restarted (§12d A network tool Planarian does not manage). Gym cases: rollback-repair-foreground-dependency; both stop spellings on display-manager from a remote administrator session get SessionLoss and ask; a rollback between two vetted generations whose manifests agree but whose NetworkManager unit differs is scheduled as ConnectivityLoss.
  • Preconditions are the executor’s. TransitionSpec::precondition is checked by the executor at §12 step 5 for every proposal, whatever its source; a failed precondition is Rejected(Precondition). The diagnoser’s use of it to filter candidates is in addition, not instead. The preconditions the spec fixes: SetOverride on a unit that already holds an Attested override by another principal fails (only an Inferred override may be replaced, and only by an authorized principal; the holder lifts their own); LiftOverride on a unit with no override fails; Revert { to } with to the current generation and Rollback with known_good the current generation fail; RestartUnit/StopUnit on an external: unit or on a unit under an override fail (I13).
  • Global floors, applied before the per-transition table, to every transition kind: recovery == NonReversible → at least AskFirst; disruption ∋ SessionLoss or DataInterruption → at least AskFirst unless the caller is an authorized user whose own session it is; disruption ∋ ConnectivityLoss → at least AskFirst unless the caller is a user with a local (non-remote) session, i.e. seat is set. “Own session” is per session, not per uid: a user dropping another of their own sessions gets the floor. other_sessions_affected with disruption ∋ SessionLoss or ConnectivityLoss, on a system-scope transition → AskFirst unless the caller holds administrator authority, and the affected sessions are notified either way. below_security_floor → AskFirst with ApproverClass::Administrator, for every transition kind. The disruption is a set and each floor applies when any member triggers it, so a change carrying several kinds meets every floor they trigger (owner decision, Nov 2; C10). So a daemon-proposed BindDriver that would drop a session, a SetRuntimeOption Wi-Fi change proposed by the diagnoser, and a non-administrator stopping the network while another user is logged in all ask.
  • Reject when: touches_core, forbidden transition, touches_canonical_probe (definition) outside AskFirst, change not in catalog, locked option, unauthorized caller.
  • Tier otherwise, by table; escalations compose with the floors:
TransitionBase→ AskFirst when
RestartUnitRuntimesensitivity ∋ Security, unless it restores an unattributed stop (residue) or restarts a failed network-security unit whose traffic is held (§12d); sensitivity ∋ External, unless the unit has already failed (above) (floors cover disruption)
StopUnitRuntimesensitivity ∋ Security or External (floors cover disruption)
SetOverride / LiftOverride / StopAndSuppressRuntimecaller is not an authorized user; or the unit’s sensitivity ∋ External; sensitivity ∋ Security and the caller is not an administrator (approver: Administrator)
BindDriverTestedsensitivity ∋ Security
ApplyChangeTestedsensitivity ∌ only Routine; touches_manifest; touches_canonical_probe; cumulative diff non-Routine (I11)
SwitchGeneration / RevertTestedtarget not live-tested or known-good; revert target differs from the transaction’s recorded revert_to; target below the security floor (administrator)
RollbackRuntimeknown_good would restore Security- or External-sensitive options that changed since (policy computes the diff), or would remove or stop a Security- or External-sensitive unit (removes_sensitivity); known_good below the security floor (administrator)
LoadModelTestednot in hash-pinned catalog → Reject
RestoreSnapshotAskFirstalways
CollectGarbageRuntimenever (I3)
SetRuntimeOptionRuntimenot in the core allowlist → Reject
  • A Rollback asks for what it would take away, not for what it restarts. Restarting a Security- or External-sensitive unit whose options are unchanged does not make it ask; restoring such an option (I11 and the row) or removing or stopping such a unit (a VPN the target lacks) does. The global floors still cover disruption (owner decision, Nov 2).
  • Autonomy setting: max_tier ∈ {Observe, Runtime, Tested}. Any Allow { tier: t, .. } with t > max_tier becomes AskFirst with ApproverClass::AnyApprover (or the class already set, if higher). Reject is never affected (I5).
  • Approval. Approval is a capability held by principals, granted only from a local human session. Whoever holds it, human or agent, may approve within its scope, but never a proposal it made itself (I19). Remote principals hold no approval capability by default. In Phase A approval requires root at an interactive terminal, and the daemon establishes the terminal itself (the controlling terminal of the peer’s process, from its pidfd). A change to merge authority, and any proposal to grant Merge, requires two distinct approvers, neither of them the proposer.
  • Deliberate actions carry identity. Every user-facing path by which an authorized principal can put a unit into a state that diverges from desired state (stop, disable, unmount, disconnect, unbind) is routed so the action is journaled as an attested Override before it takes effect: the CLI and console in A; in C, PlanarianOS also wraps the commands people type (systemctl, nmcli, udisksctl) and routes the desktop’s own switches through the same path. Logging out and suspending are not divergences (conditions stop holding) and need no override. A root nixos-rebuild outside Planarian is observed as a new, unconfirmed base: not fought, not trusted.
  • The unattributed residue. A raw signal or direct sysfs write has no principal. OOM kills (Result=oom-kill) are always faults. A stop of a Security-sensitive unit is always a fault: it is restarted and alarmed, never inferred as intended. So is a stop of a unit whose manifest sets inferStops = false (default true; the reference configuration sets it false for backup, sync, and the desktop’s own units). inferStops is provisional (§19): if unit importance comes to matter beyond residue inference, replace it with a declared importance property separate from sensitivity. Otherwise, by posture: Relaxed and Standard accept the stop until next boot as an Override { by: Actor::Unattributed, attribution: Inferred, until: NextBoot }, with a notice and an undo; Guarded and Sealed open the 10 s ask window and restart if nobody answers. An inferred override is an observation the daemon records, not a proposal: it does not pass through decide, holds no authority, and cannot be extended or converted to Attested except by an authorized principal setting a new override. The journal records the inference as an inference, and the console lists inferred overrides until a user acknowledges or undoes each one.
  • Sensitivity completeness. Overrides, the residue rule, and I22 key on Security, so an untagged security unit is unprotected. Units declared through the security.*, networking.firewall.*, services.openssh.*, services.fail2ban.*, services.usbguard.*, and VPN option trees are Security by default; a unit with no sensitivity entry is treated as non-Routine (fail closed). A gym entry checks a named list of security units against the shipped tables.
  • Disruption surfacing: any Allow with disruption ≥ SessionLoss is logged and, if a console is attached, shown before execution with a 10 s cancel window for daemon-initiated actions.
  • PolicyVersion is the hash of the policy crate build plus the shipped sensitivity tables; it is recorded in every transaction and token.

§12. Executor and transaction kernel (crates exec, txn, watchdog)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Journal: append-only, write-ahead, fsync’d, in a preallocated file at /var/lib/planarian/journal/ (§12c). Read-only backend calls (current, boot_entries, last_booted, observe, stage, evaluate, build — build writes only to the Nix store and activates nothing, so it is treated as read-only with respect to system state, but its output is recorded) may precede Prepared. Every mutating call (activate_live, publish_boot, protect, restart, stop, snapshot, restore, file promotion) requires a durably written Prepared record and a current lease epoch. Prepared is written on the Allow path as well as the AskFirst path.

Ownership and fencing. Each transaction has exactly one owner at a time (Lease). Mutating backend calls carry the lease epoch; the backend layer reads the journal’s current epoch for that transaction and refuses the call if it differs (Outcome::Failed("fenced")). Mutations that take time (activation, revert) run in a worker (a thread in A, a subprocess recorded in the lease from D; see “Worker processes” below) that inherits the epoch. Taking over a transaction means: write a new lease with epoch + 1 (durably), stop the previous worker where one can be addressed (SIGTERM then SIGKILL to its process group via pidfd, from D), then establish confirmed quiescence. Every mutation attempt first journals a durable operation id (OpId, an OpBegin { txn, epoch, seq } record written and fsynced before the backend call, matched by an OpEnd with the outcome after it; “acknowledged” below means the OpEnd exists), and the backend call is tagged with it (for systemd: the job’s unit is started with a transient property X-Planarian-Op=<OpId> via SetUnitProperties in the same D-Bus transaction, or, where the call cannot carry a tag, the daemon records the pre-call snapshot of pending_jobs() as the op’s baseline). Quiescence then covers both the jobs in the old lease’s jobs list and every job discovered by jobs_tagged(op) for each journaled OpId whose acknowledgment was never recorded. Every such job must be cancelled (systemctl cancel <job> where the supervisor supports it) and confirmed gone from pending_jobs(), and any activation worker’s own child processes reaped; the new owner waits up to the job-drain limit (60 s) for that confirmation, and if quiescence cannot be confirmed, or jobs_tagged returns None for an unacknowledged OpId (the backend cannot say whether undiscovered work remains), it writes RecoveryFailed and blocks all further mutation rather than proceeding. A drain timeout is never permission to proceed, and neither is an empty list of known jobs. Gym case: accept an activation job, withhold its reply, kill the owner before the job id is journaled, start recovery; recovery must discover or fence the accepted work before any conflicting mutation. Only after confirmed quiescence may the new owner advance the journal. Lease transfer is atomic: the lease is a file /var/lib/planarian/txn/<txn>/lease replaced by atomic rename of a temp file whose name embeds the candidate epoch, after flock on the transaction directory; two processes attempting takeover serialize on the lock and the second sees the first’s epoch and backs off. An old owner that wakes up finds its epoch stale on its next call and exits. Only the current lease holder may advance the journal or commit. Gym case: kill and immediately restart the daemon while a deliberately slow activation worker survives; assert activation and revert never overlap and the old owner cannot commit afterward.

Protocol for a Proposal:

  1. Observe S0. If proposal.observed != S0.digest() → Reject Stale, return the current digest.

  2. Admission before work. Before any staging, evaluation or build, check what can be checked cheaply: the caller’s identity and capabilities against the transition’s authorization rule (§11), the catalog membership of an ApplyChange, and the recovery interlock (§12c). A caller who cannot be authorized for the transition causes zero stage/evaluate/build calls (Reject(Unauthorized) here). Then, for ApplyChange: run the handler, stage, evaluate, and build (read-only path); for SwitchGeneration/Revert/Rollback: dry-activate the target (§11). Derive facts from the evaluated candidate. Work in this step is charged to the caller and bounded (one build slot per caller class; §16a).

  3. Refinement check: proposal.expected MUST agree with spec.predict() on every unit the spec predicts, MUST NOT broaden may_change, MUST NOT set others_unchanged = false where the spec sets true. Violation → Reject. Executor-owned requirements (§9) are appended. The transition’s RecoveryStrategy and revert_to are fixed here by the spec, not the proposer.

  4. policy.decide (with authorization). Reject → log, alert if the source was the model. Otherwise create the transaction atomically: mkdir /var/lib/planarian/txn/<txn>/ (fails if it exists), write base.nix and the initial lease (epoch 1, this daemon) into it, then journal Prepared (durable) with facts, expected, revert_to, recovery strategy, handler, staged_source, base/candidate override hashes, gc_root, candidate path, policy version, and the lease. The transaction does not exist until the Prepared record is durable; the directory alone is garbage the next startup removes. AskFirst → request a token bound as in §8, park. On token: re-derive facts, re-run decide, recompute cumulative hash and candidate verification; any difference in bound_to invalidates the token and re-requests. Tokens expire in 24 h (CLOCK_BOOTTIME, §12h), are single-use, and do not survive a daemon restart (owner decision, 30 Oct): a restart invalidates every unused token, the parked change re-requests approval, and the checkpoint carries consumed nonces only, for replay protection. The approval is recorded: the token-use journal record and the transaction carry the approver’s PrincipalId, bound_to and the transaction id, and the approved execution writes its own log record (I7).

  5. Check precondition against the current state (TransitionSpec::precondition; §11). A failed precondition writes Rejected(Precondition); an expired or invalidated approval that is not re-granted within its window, or a policy decision that changes to Reject on re-evaluation, writes the terminal Rejected(reason); all release the GC root. A user withdrawing a parked proposal writes Cancelled. All are terminal and reopen the incident if one was waiting.

  6. (The lease was created with the transaction in step 4.) Snapshot if facts.sensitivity ∋ UserData and supported; record. If unsupported in Phase A, log NoSnapshot and continue; from C, AskFirst.

  7. Execute, by recovery strategy, which is fixed per transition and independent of tier:

    TransitionHandlerRecoveryNotes
    RestartUnit, StopUnit, StopAndSuppress, SetOverride, LiftOverrideUnitOp / IntentOpReobserveOnlycandidate = None; never a generation operation
    SetRuntimeOption, CollectGarbageUnitOp / GcOpReobserveOnlysame
    ApplyChange, SwitchGeneration, Revert, LoadModelGenerationOpRevertGenerationfull journal path below
    RollbackGenerationOpRevertGenerationtier may be Runtime, but it is a generation operation: serialized (I6), owned, with candidate = known_good (or accepted_baseline), publish_boot, and override restoration to the override content recorded for that generation; its own revert_to is the generation it left
    BindDriverDeviceOpUnbindDevicea device operation via the inventory backend (bind/unbind on the sysfs driver interface); no generation is built or activated; on failure or mismatch, unbind and rebind the previously bound driver (recorded at Prepared)
    RestoreSnapshotSnapshotOpRestoreSnapshotThenRevertC
    • UnitOp / IntentOp / GcOp (ReobserveOnly): journal Activating → backend call in-process → Checking → settle → observe → compare → journal Committed with the outcome recorded, whether or not the expectation held; a mismatch informs the incident and never triggers generation reversion. A Partial/Unknown outcome means re-observe and record, nothing more. StopAndSuppress journals the suppression before calling stop.
    • GenerationOp (RevertGeneration), boot-only candidates (§12f: the change takes effect only at boot): verify_candidate → journal Publishing (durable, before any boot mutation) → publish_boot_once(candidate) as a one-shot next-boot entry, with the running generation (the one publishing the candidate; always at or above the security floor) kept as the default and fallback (owner decision, 30 Oct; known_good is a manual fallback per I1, never the automatic one) → journal AwaitingBoot. At the next boot, the candidate boots once; the admission check and the confirmation checkpoint of §10 apply: admitted → the candidate is made the default and the override promoted (§9 step 4) → Promoted; confirmed at 10 min → post-commit bookkeeping → Committed (boot-confirmed, known_good); not admitted, or the boot does not reach ready: the following boot falls back to the publishing generation → Reverted; admitted but not confirmed (canonical probes fail at the checkpoint) → Reverting: publish_boot(publishing generation) as default, restore the base override, the notice says a restart will complete it → Reverted (the machine runs the candidate until that restart; the daemon treats the window as a live-tested-but-unconfirmed generation). A daemon restart without a reboot leaves AwaitingBoot untouched and never re-prepares it.
    • GenerationOp (RevertGeneration), live: wait for an activation window if §12f requires one (the transaction stays in Built, bounded per §12f) → when the window opens: stale check, re-derive, re-decide (I8) → from B, confirm a passing sandbox run for this staged_source where §12g requires one → verify_candidate → journal Activating → worker: activate_live(candidate) → journal Checking → run applicable Readiness probes from both the candidate’s UnitDefs and the installed core, plus the expectation (per probe, §9) → pass: publish_boot(candidate) → journal Published → promote (§9 step 4) → journal Promoted → post-commit bookkeeping (§12e floor derivation, the committed generation’s override content, session accounting; part of the commit, I25) → journal Committed (same record marks live_tested). Fail: journal Reverting → worker: activate_live(revert_to) → publish_boot(revert_to) → restore base override (§9 step 4) → re-probe → Reverted, or RecoveryFailed if revert probes fail (alerts; stops all autonomous action until a human acts).
    • DeviceOp (UnbindDevice): journal Activating with the previously bound driver recorded → bind → Checking → probes → pass: Committed; fail: Reverting → unbind, rebind previous → Reverted/RecoveryFailed. Gym case: a failed driver bind restores the previous driver and attempts no generation rollback.
    • RestoreSnapshotThenRevert (RestoreSnapshot): restore, then as above.
  8. Outcomes. Partial/Unknown are never success. In Activating with RevertGeneration: re-observe; candidate active → Checking; else → Reverting. In Checking: probes decide. In Published/Promoted on the live path: the candidate is already live and tested; recovery completes the commit (re-run publish_boot, re-run the promotion rename — both idempotent) rather than reverting, unless probes now fail, in which case revert. In Promoted on the boot-only path the candidate is admitted, not confirmed: recovery waits for the confirmation checkpoint of this boot and then commits or reverts as above; it never writes Committed before it. The watchdog may complete these steps because they are idempotent and were authorized by the transaction; it still cannot write Committed from any state before Checking.

  9. Log (I7) with DerivedFacts, PolicyVersion, probe evidence, decision, outcome, lease epoch.

Crash-recovery table (what the lease holder does on finding a transaction in each state):

Found inAction
Prepared, Staged, Built (any strategy)Nothing was mutated; mark Rejected(Interrupted), release the GC root, reopen the incident if any. A transaction that was waiting for an activation window is re-prepared from the same proposal, with its original caller and source (not as the daemon’s maintenance), once the daemon is ready; any unused approval token is invalidated by the restart and re-requested (§12 step 4)
Publishing (boot-only)Read the publish_boot_once op’s records: OpEnd succeeded → journal AwaitingBoot; no OpEnd → ask the backend whether the one-shot is armed (boot_entries); armed → AwaitingBoot, not armed → Rejected(Interrupted). Never re-prepare a transaction that reached Publishing
AwaitingBootNo reboot since publish: leave as is. After a reboot: last_booted() is the candidate → the admission check of §10; any other generation has booted since the publish → Reverted
Activating, ReobserveOnlyFence; confirm quiescence; re-observe; write Committed with the observed outcome; inform the incident. Never a generation operation.
Activating, RevertGenerationFence; confirm quiescence; re-observe; candidate active → Checking, else Reverting
Activating, UnbindDeviceFence; confirm quiescence; re-observe; bound to candidate driver → Checking, else restore previous driver → Reverted
Checking (generation / device)Fence; confirm quiescence; re-run probes; pass → the complete commit path, fail → Reverting
PublishedPromote by hash comparison (§9) → Promoted → post-commit bookkeeping → Committed, unless probes fail → Reverting
Promoted (live)Post-commit bookkeeping (I25) → write Committed. Whoever holds the lease — daemon or watchdog — runs the bookkeeping; a Committed without it is not written
Promoted (boot-only, this boot)Admitted, not confirmed: wait for the confirmation checkpoint (§10); pass → bookkeeping → Committed; fail → Reverting as in step 7. The watchdog does not advance this row; only the daemon’s checkpoint does, and if the daemon is absent at the checkpoint the next boot runs the AwaitingBoot row
RevertingFence; confirm quiescence; re-run the strategy’s revert steps (all idempotent) → Reverted or RecoveryFailed
Checking, ReobserveOnlyFence; confirm quiescence; re-observe; write Committed with the observed outcome. Never the generation commit path.
RecoveryFailedAlert; set the recovery interlock (§12c); refuse all autonomous transactions until an administrator records planarian resolve <txn>

Gym cases: kill the daemon during a Runtime-tier Rollback (recovers as a generation operation) and during StopAndSuppress (recovers as a unit operation; no generation rollback attempted); an old backend job that outlives the 60 s drain (takeover ends in RecoveryFailed, never overlapping mutation); daemon and watchdog attempt takeover simultaneously (exactly one wins); daemon stays down after Published and after Promoted (watchdog completes the commit).

Recovery watchdog. planarian-recovery.service: separate unit, Restart=always, started before planariad, no dependency on it. Every 10 s it reads the journal; for a transaction in Activating/Checking/Publishing/Published/Promoted (live)/Reverting whose lease is unrenewed past deadline or whose owner pid is dead, it takes over the lease (fencing and confirmed quiescence as above) and runs the crash-recovery table. From Checking it does one of two complete things: if the authorized Readiness probes pass, it runs the entire authorized commit path (publish_boot → promote → Committed), because that path was authorized by the transaction and every step is idempotent; otherwise it reverts. It never stops partway. The earlier restriction is restated precisely: the watchdog may not write Committed from any state before Checking; from Checking, Published or Promoted (live) it completes the authorized path, bookkeeping included (I25). For a daemon that is alive and renewing its lease, the watchdog does nothing. Its binary and config are Sensitivity::Core.

Daemon restart reconciliation. On start, planariad reads the journal. If the watchdog holds a lease on a non-terminal transaction, the daemon waits until that transaction is terminal (it does not compete). Otherwise it takes over the lease (fencing) and runs the crash-recovery table. Either way it writes a reconciliation record (what it found, what it did, why) shown in the console on next open. Recovery steps that may activate a generation (a transaction found in Activating, Checking, Publishing, Published or Reverting) run just after planarian-ready.target is reached: the takeover and the reconciliation record come before it (owner decision, Nov 1). Activation reloads and restarts units ordered after the target, which would otherwise wait on planariad’s own start (#9).

Self-protection during activation. The NixOS units for planariad and planarian-recovery are declared with restartIfChanged = false and stopIfChanged = false, which NixOS’s switch honors by emitting X-RestartIfChanged=false/X-StopIfChanged=false, so no activation restarts or stops them. Independently, a candidate whose evaluated unit files for these two services differ from the running generation’s is touches_core → Reject; upgrading Planarian itself is the core update path (Phase E), and in A–D a manual nixos-rebuild by root outside Planarian. Networking changes set deadline to 90 s because loss of the daemon’s control path is likely. Boot ordering, readiness, and the response to the daemon failing are in §12d.

Concurrency: I6. Runtime transitions may run during build but not during Activating/Checking/Publishing/Published/Promoted/Reverting, except a boot-only Promoted, during which unit and device repairs run (owner decision, 31 Oct). This list and I6’s are the same list; schema carries it once.

Worker processes. v4.1–v4.5 described a worker subprocess whose pid the lease records. In A the activation runs in a worker thread and quiescence is established through jobs_tagged and the transient-unit tag alone, which is sufficient; the Lease.worker_pid and Lease.jobs fields are reserved for D’s supervisor and are empty in A (recorded in docs/deviations.md).

§12a. Runtime-settable options

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A core-owned allowlist (Sensitivity::Core; not extensible by manifest or model) of options whose effect can be applied immediately: font scale, volume, brightness, keyboard layout, Wi-Fi network selection. SetRuntimeOption applies the effect as a Runtime transition (subject to the global floors: Wi-Fi selection has ConnectivityLoss, none while NetworkManager has already failed, §11), then enqueues an ApplyChange write-back whose expectation is “no unit changes.” If the write-back’s build or probes fail, the runtime change stays in effect, the write-back is Reverted (override source untouched), the console shows “this setting will not survive reboot,” and the incident manager does not retry it; the user may re-issue it. The write-back is never attempted while a generation transaction is active: it is queued behind the active transaction and submitted when that transaction is terminal, with the same caller. A write-back refused as busy is not a failure and produces no notice.

§12b. Garbage collection and space budgets

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Protected set per I3, including in-flight candidates (via their GC-root symlinks) and every txn/<txn>/base.nix of a non-terminal transaction. services.planarian.gc.retainAllGenerations (default false) protects every generation: a full history kept for export or to go back to any of them; other store garbage is still collected, and the budgets below are reported, not enforced. Policy: keep last 10 generations and 10 snapshots and space budgets (default: generations ≤ 20% of the Nix store’s filesystem, snapshots ≤ 10% of their volume); whichever bound is hit first collects oldest-first down to the other. If the protected set alone exceeds a budget, CollectGarbage collects everything unprotected, reports BudgetUnsatisfiable with the shortfall, and never touches the protected set; the budget is then a target, not a guarantee, and the console says so. CollectGarbage is Runtime. Budgets are user settings, not manifest options.

§12c. Journal headroom and ENOSPC

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • Format. Records are length-prefixed and CRC-32C-checked; a torn or mismatched tail record is ignored and truncated on open, never interpreted as a transition. Anything worse — a checkpoint that fails its CRC, does not parse, or violates an invariant on load, or a retained record that does — is an Integrity failure (§12d), reported as such by both processes; it is never reported as the journal being merely absent. Every record also carries the hash of its predecessor (I18), and the checkpoint carries the chain head, so an edit to any retained record is detected on the next read and raised as an Integrity failure (§12d). The chain is tamper-evident, not tamper-proof, until E signs the chain head with a key outside the running system. The journal is a preallocated file (default 64 MB) in /var/lib/planarian/journal/.
  • Journal versions (owner decision, Nov 3). Two classes of record kind. Every journal record kind is one of: informational: no decision reads it; decisive: the fold uses it to decide what runs, what is held, or what is approved. Hold events and withdrawal links are decisive. The class is in the frame. Each record’s frame carries its kind’s class, so a build can classify a kind it doesn’t know. Opening a journal with an unknown kind. An unknown informational record is skipped, and the skip is journaled (Skipped { kind }). An unknown decisive record is an Integrity failure. Rollbacks across a format change. A generation transition whose target planariad cannot read a decisive record kind present in the journal, or a decisive field of one (a field added to an existing kind that the fold decides from), asks first (AskFirst, ApproverClass::Administrator). The approval names what the target would not understand, so a rollback never silently lands a machine in Integrity or drops a decision. Each build exports the kinds and decisive fields it reads (journalKinds). A generation from before the export is taken to read only the journal format’s original set.
  • Chain head. The chain is unkeyed in A: it detects damage and partial edits, not a consistent rewrite by root (I18). The current head is included in every signed health report (§10), so an off-machine receiver that keeps the heads it has seen can detect a rewrite of anything it has already witnessed (C, with the mesh); measured boot anchors it on the machine in E.
  • Checkpoint. Compaction is not deletion of history; it is checkpoint-then-truncate. A checkpoint captures all live state the journal establishes: the external desired-state source’s last accepted sequence number (§9), active overrides (including Until::Explicit ones from long-committed StopAndSuppress), the security floor, open and cooling incidents with their budgets and regression counts, the nonces of consumed approval tokens within their validity window (outstanding tokens are not carried: a restart invalidates them, §12 step 4), the AgentStarted records of live agent instances (I24), known_good/accepted_baseline/live_tested, every non-terminal transaction with its lease, the lease epochs of recently terminal ones, and the recovery interlock: the set of unresolved RecoveryFailed transactions with the facts needed to resolve them (what was being changed, which step failed, the observed state, the base and candidate override hashes, the revert target). The interlock is live control state, not history: while it is non-empty, autonomous mutation is blocked (only Observe-tier reads, user-authorized SetOverride/LiftOverride, and administrator operations run), and it survives compaction and restart of both processes. It is cleared only by planarian resolve <txn> --as-committed | --as-reverted | --manual-repaired, an administrator-authorized operation journaled as a transition of its own; no other request clears it. Gym case: inject a failed revert, force compaction, restart daemon and watchdog; autonomous mutation stays blocked until the resolution is durably recorded. It is written to a temp file, fsynced, renamed over checkpoint, directory fsynced; only then is the journal truncated to records after the checkpoint’s sequence number. Recovery from (checkpoint + retained journal) MUST reconstruct the same state as recovery from full history; the gym asserts this by comparing both reconstructions after forced compaction. Compaction triggers at 75% full.
  • Filesystem contract. The preallocation guarantee holds only where in-place rewrite does not allocate: on ext4 and XFS a fallocated file; on btrfs only with the journal directory marked nodatacow (chattr +C before creation) and the file preallocated; on ZFS it does not hold, and the daemon instead reserves space with a dedicated dataset reservation. The installer applies the layout-specific setup and the gym validates it per supported layout (ext4, btrfs, ZFS). If a recovery-path write to the preallocation fails with ENOSPC despite the contract, the daemon writes a fixed-size RecoveryFailed sentinel into a separate 4 KB file reserved at install and blocks all mutation. The reservation is counted in device:fs-journal’s Readiness probe.
  • CollectGarbage { emergency: true } is the only transition allowed to start while device:fs-journal or device:fs-nix is below threshold; it is Runtime and its record fits the preallocation.
  • ENOSPC behavior by state: in Prepared/Staged/Built (write to stage or store fails) → reject with NoSpace, open the storage incident, do not retry the original until it closes. In Activating/Checking → treat as probe failure → Reverting; the revert record is in the preallocation. In Reverting → if a revert step fails for space, mark RecoveryFailed via a fixed-size sentinel record and alert. Promotion (temp-write of the override file) failing for space → treat as Checking failure → revert; the live override file is untouched because the rename never happened.
  • /boot is tracked separately: publish_boot failing for space → Checking failure → revert; the candidate is never left as the only boot entry.

Gym cases: fill the journal filesystem to the reserved-block boundary as the daemon’s uid; verify that emergency collection is recorded and executed, or that mutation stops with an explicit RecoveryFailed/NoSpace status; repeat after a snapshot and after repeated journal reuse on btrfs; force compaction with a live Explicit suppression and an exhausted incident budget, then crash and restart (both survive); interrupt a record write and a checkpoint write (neither is misread).

§12d. Boot ordering and failure posture

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Admit no principal before the monitor. planariad is ordered before every unit that admits a principal: the display manager (whichever unit NixOS uses for it — display-manager.service, or greetd.service where services.greetd is enabled without the display-manager alias), getty, sshd, the model service, any remote relay, user@.service, and every agent unit, declared or launched. Agents started by planarian agent run are created in a planarian-agents.slice with After=/Requires=planarian-ready.target and a start condition that fails while any planarian-failed@*.target is active; the posture’s disable agents response stops that slice as a whole, not a configured list, so an agent the launcher created cannot outlive a failure posture (I20). Each launch carries TasksMax=, MemoryMax= and a per-uid count from services.planarian.agents.limits. When it is ready it reaches planarian-ready.target. Whether principal-admitting units merely follow that target or require it is the failure posture. With this ordering, the window before the monitor is ready contains only firmware, bootloader, kernel, initrd, and the generation’s own early units, all already in the trusted base; and the change path fails closed while planariad is down, since Proposal is the only write path.

Enforced outside the daemon. The NixOS module generates planarian-ready.target and one planarian-failed@<class>.target per failure class, wired with ordering and OnFailure= in systemd and the initrd. planariad never implements its own failure response. The posture is declared in the generation, so it is immutable at boot.

ClassMeaningDetected by
Absentplanariad did not reach ready within the boot-ready timeout, or was masked or stoppedordering timeout on planarian-ready.target
Crashedreached ready, then died and exhausted its restart budget; or planarian-recovery exhausted its own start limitOnFailure= on both units, each with a StartLimitBurst so a crash loop ends in the class rather than running forever
Integritycore hash ≠ signature (I2); journal chain broken (I18); boot report found undeclared early units or processes; invalid signature on the external desired-state source (§9); in E, a measured-boot mismatchboot report, planarian-recovery’s independent check, initrd measurement

Integrity is always treated at least as seriously as Absent.

Responses (composable): alarm (a non-dismissable banner on the login screen and in every session until an administrator acknowledges it: for Absent/Crashed, “Planarian, which protects this computer from unsafe changes, is not running. Assistants and system changes are turned off until it is fixed. [Administrator] has been notified.”; for Integrity, “This computer may have been tampered with. Do not enter passwords or open sensitive files. [Administrator] has been notified.”); notify administrators, local and remote, with the boot report; disable agents (the model service, any relay, every agent principal are not started); restrict remote access to administrators; restrict login to local administrators in a recovery session; restrict network (none, mesh-to-administrators, or full); halt to recovery (boot stops at a recovery target); in E, seal (the disk-encryption key is sealed to measurements that include the core, so an Integrity failure leaves the disk locked).

PostureForAbsent / CrashedIntegrity
RelaxedDeveloping Planarian; throwaway VMsAlarm (dismissable), disable agentsAlarm, notify, disable agents
Standard (default)Personal machinesAlarm, notify, disable agents, restrict remote access; local login proceedsAlarm, notify, disable agents, restrict remote access, network to mesh-to-administrators (in A, before any mesh exists: remote access off, network otherwise unrestricted); local login proceeds with the warning
GuardedWork machines; agent hostsAlarm, notify, disable agents, restrict remote access, restrict loginHalt to recovery, network to mesh-to-administrators
SealedServers, fleet nodesHalt to recovery, network to mesh-to-administratorsHalt to recovery, no network, seal (E)

Every posture disables agents on any failure (I20). Restrict remote access to administrators means Planarian’s administrators (root and planarian-admin), not wheel. In A, alarms and notification are local (the login screen, every session, wall, the system journal): root can clear them, and a machine whose planariad is stopped can go unnoticed by anyone not at it. From C, the signed health report doubles as a dead-man signal: an off-machine receiver alerts administrators when reports stop arriving within twice the declared cadence. Custom postures combine the options per class, with two constraints: agents are always disabled, and the Integrity response is never weaker than the Absent response. The installer selects the posture (C; A ships Standard). The boot-ready timeout is configurable per machine, default 60 s, bounded 15–600 s. Raising the posture or shortening the timeout is a Tested change; lowering the posture or lengthening the timeout is Security-sensitive (AskFirst with administrator authority) and takes effect at the next boot, never in the current one: the posture units read the declared posture from the booted generation’s files, and a switch that would weaken either leaves the booted values in force until reboot (the module implements this by having planarian-ready.target’s configuration come from /run/booted-system, not /run/current-system). Recovery exits: an administrator acknowledges the alarm (journaled with identity), or planariad reaches ready and the boot report is clean, after which restrictions lift. Only an administrator clears an Integrity failure.

Network security fails closed (owner decision, Nov 2). Which units. A network tool is a unit able to change how traffic leaves the machine: one that holds CAP_NET_ADMIN or CAP_NET_RAW, or could gain either (its bounding set allows it and NoNewPrivileges= is unset, so a setuid or file-capability binary such as ping can grant it), can open /dev/net/tun or /dev/ppp, declares network interfaces, or runs as root without a capability bounding set (computed at build time from the unit’s systemd configuration, never from its name; fail closed). A network tool is protecting traffic while the kernel has attributed to it a change that currently steers traffic: an interface it created that carries a default or all-covering route, a default or all-covering route it added over a virtual interface it did not create (a persistent tun or tap made by a setup unit, or one moved in from another network namespace; the creator, where known, is protecting too), a policy rule catching all traffic, DNS servers on its interface, the system proxy, or firewall rules that drop or redirect traffic. Planarian attributes every routing, interface, rule and nftables change to the unit that made it from inside the kernel (eBPF, written with aya, on the netlink receive paths, the legacy netfilter setsockopt path, and network-device registration, so an interface created by ioctl such as a tun is attributed too; only changes in the host’s network namespace count; crates/backends/netwatch), and keeps each unit’s current footprint in /run. A network-security unit is a network tool that is protecting traffic. Its own interfaces and peer endpoints, read from its footprint, are what the hold passes; holdInterfaces and holdAllow may add to them. Fail closed: a change the kernel could not attribute, a footprint missing or stale when a network tool fails, or a watch that failed to attach, all count as protecting. Network tools start after the watch (owner decision, Nov 2). Every network tool is ordered after planarian-netwatch.service. That service reports ready only once its probes are attached and its first footprints are written, so no network tool can change how traffic leaves the machine before the watch sees it. The ordering is not a dependency: if the watch fails to start, network tools still start, and the watch counts as failed (fail closed, above). The watch in the initrd (owner decision, Nov 3). With the systemd initrd, the watch also runs in the initrd, ordered before every network tool there. Its probes and event queue stay in the kernel across the switch to the root. The stage-2 watch takes over the initrd’s footprints and the events queued meanwhile. So no network tool, in either stage, acts unwatched. If the stage-2 watch cannot take over (a version mismatch, or the probes are gone), it attaches afresh, and anything it cannot attribute counts as protecting. A unit the watch itself needs in order to start cannot wait for it. The module computes those units, lists them in the export (networkToolsUnordered), and rejects a configuration in which the ordering would form a cycle; for those units, a failure before the watch attaches holds traffic. With a scripted initrd, the initrd runs no network tools. When a network-security unit has failed (observed Failed, or not running while it is desired running; a stop through an attested override is not a failure), its traffic is held: an nftables table drops all traffic in and out except loopback, address assignment (DHCP, router advertisements), traffic to the endpoints the unit’s manifest entry declares in holdAllow, traffic over the unit’s own interfaces (holdInterfaces: while it is down they carry nothing, and once it is back its traffic goes only through it, as a VPN kill switch does; owner decision, Nov 2), and established connections of administrator sessions; new connections, new SSH logins among them, are refused. The hold is applied by systemd through OnFailure= on the unit (a crash or a kill) and by planariad for a stop outside Planarian that is not attributed (always a fault, §11 residue), so it does not depend on the daemon; the table is Sensitivity::Core, survives a restart of planariad, and is in place before the incident opens. The unit’s restart runs at once, with an alarm (“ stopped unexpectedly. Network traffic is blocked until it is back.”) (§11), if Planarian manages it; otherwise see A network tool Planarian does not manage. The hold is lifted when the unit is Running and its Readiness probes pass. If repair fails (the attempt budget is exhausted, or RecoveryFailed), the hold stays and administrators are notified: the machine never falls back to unprotected traffic on its own. An authorized principal may lift a hold with planarian hold release <unit> [--until], journaled as an attested Override on the hold; a hold protects a Security-sensitive unit, so releasing it needs administrator authority, and a non-administrator’s release is held as AskFirst with ApproverClass::Administrator (§11). Gym cases (VM): kill a WireGuard unit, then new outbound connections fail, loopback works, an administrator’s established SSH session survives and a new SSH login is refused, the restart restores the tunnel and the hold lifts, with one alarm; the same with the restart failing every attempt, then the hold stays, administrators are notified and nothing is reachable; planarian stop on the unit holds nothing; a non-administrator’s hold release asks for an administrator; at boot, every network tool becomes active only after the watch is ready, and nothing is unattributed; with the watch failing to start, the tools still start and a failing one is held; with the systemd initrd, every network tool the initrd runs is attributed (journald, udevd, module loading), and no footprint carries pre-attach after switch-root.

A network tool Planarian does not manage (owner decision, Nov 3). Such a unit is a network-security unit with no services.planarian.units entry. When it fails: it is held like any other; Planarian does not repair it, because it repairs only declared units; the hold lifts when the unit is Running again by other means, or on hold release; the alarm says it needs a person: “ stopped and is not managed by Planarian. Network traffic is blocked until it is back or an administrator releases it.”

Core hash in A–D. The reference hash for I2 covers every Planarian binary and script the generation installs (planariad, planarian-recovery, the CLI, the posture units’ scripts), recorded at build. Signatures replace it in E. A test build (the gym-hooks package, §15) installed outside the gym VM tests is an Integrity failure, and the module asserts against it.

Lock the boot chain where the machine allows it: bootloader editing disabled (boot.loader.systemd-boot.editor = false); Secure Boot with a signed unified kernel image (Lanzaboote) from C; TPM measured boot later. In C, pre-Planarian boot entries are garbage-collected after enrollment. Entries below the security floor (§12e) stay in the boot menu (I1) but are labeled, are never the automatic fallback, and, where the bootloader supports it, require administrator confirmation.

§12e. Security floor

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The security floor is the newest committed generation whose closure contains a security fix absent from every earlier generation (in A: absent from the current floor generation and from every generation between it and the candidate; a Revert to an older generation that merely lacks a newer package is therefore never a fix). It is a derived fact, computed as part of every generation commit on every commit path (I25) and journaled before Committed; a commit whose floor record cannot be written does not commit. It is never set by a proposal. The inspected set is the whole system closure: every package reachable from the generation’s toplevel, including service packages, the kernel, and firmware, not only environment.systemPackages. In A the sources are nixpkgs vulnerability metadata, evaluated for every package in the closure (a package whose previous version carried knownVulnerabilities that the new one lacks), and an administrator mark (planarian mark-security-floor); from B, a CVE feed matched against the closure (e.g. vulnix) is added. A fix the inspected set does not show does not move the floor; the administrator mark is the remedy. Generations below the floor are not eligible as automatic revert targets, as automatic boot fallbacks, or as Rollback targets; going below the floor is AskFirst with ApproverClass::Administrator (I16). Until the floor generation is boot-confirmed, known_good may be below the floor: it stays bootable and in the menu (I1) as a manual, administrator-confirmed fallback, while the floor generation is the boot default. The floor never advances to a generation that has not at least committed live. If no eligible known-good generation exists above the floor when a revert or rollback is needed, the daemon escalates the incident with EscalationReason::RevertBlockedByFloor instead of reverting. Gym cases: a security update is applied; a probe failure is induced afterward; the revert does not cross the floor and the incident escalates. The same, with the fix in a service package (openssh) that is not in environment.systemPackages. The fixing generation commits via the daemon, via the watchdog after a crash at Committed-minus-one, and via the boot-only path; all three move the floor, and a Rollback to the vulnerable generation then asks. A Revert to an older generation sets no floor.

§12f. Activation scheduling

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

A verified candidate whose activation would restart or reload any unit in an active user session (audio, compositor, input, network, the session itself) waits in Built until an activation window: the session is idle, the user asks, or the next boot. It never activates while Foreground holds in any session, except a rollback the user or an Intent requests, or a repair rollback, as below. Changes that only take effect at boot (kernel, initrd, drivers) take the boot-only path (§12, step 7): published as a one-shot next-boot entry in AwaitingBoot, with a non-urgent “restart to finish” notice, and confirmed or reverted at that boot. Repairs are exempt: restoring a failed unit is not an activation. A transaction waiting in Built holds no lease-gated mutation and counts as active for I6, so §12a write-backs queue behind it; if the daemon restarts, it is Rejected(Interrupted) per the crash table and re-prepared with its original caller. “The next boot” as a window therefore means: re-prepared after boot and activated before any user session starts. Parking is bounded (owner decision, 30 Oct): a transaction may wait in Built for at most the parking limit (24 h, CLOCK_BOOTTIME); when it expires the transaction is Cancelled(Daemon) with a notice naming the change and how to re-issue it, and the incident, if any, reopens. AwaitingBoot has no limit — it waits for the reboot it was published for — but the console and planarian status report it as waiting for a restart, distinct from any failure, and the “restart to finish” notice is repeated daily. I12 is qualified accordingly.

Rollbacks (owner decision, Oct 31). A Rollback with Source::User or Source::Intent (planarian undo, “roll back”) runs at once: the request is the activation window, also while Foreground holds (owner decision, Nov 2). A Rollback with Source::Diagnoser, proposed after in-place repair of an incident has failed (§13 orders it last), is a repair: it does not wait for an activation window, and is not parked in Built. Being a repair exempts it from waiting, never from asking: the §11 disruption floors apply to it as to any transition, so a repair rollback that would end a session is AskFirst. While Foreground holds, a repair rollback runs only if its incident’s target is one the foreground work plausibly depends on (the list under Quiet mode); otherwise the diagnoser does not submit it — it is deferred with the incident, as quiet mode defers any repair, with no transaction opened, and runs when quiet mode ends. The revert of a candidate that fails its own checks (Reverting, §12 step 7) is that transaction’s recovery, not an activation, and is never deferred. I6 still holds: a rollback is a generation operation. When a repair rollback, or a rollback the user or an Intent requests, is about to run (admitted, and approved if it had to ask), an active generation transaction that has not yet mutated (one waiting in Built for its window) is withdrawn for it, Rejected(Interrupted), and prepared again once the rollback is terminal; one past Built is waited for, like any other. A rollback still waiting for approval withdraws nothing (owner decisions, Nov 2). Gym cases: rollback-repair-foreground-dependency (a committed update breaks Wi-Fi during a Foreground workload; the rollback runs at once, with one notice); rollback-repair-foreground-unrelated (the same update breaks the printer service; the rollback waits for the workload to end, then runs, and the incident’s budget does not advance meanwhile); rollback-repair-session-loss-asks (a repair rollback that would restart the compositor of an active session is AskFirst and does not run without approval).

Quiet mode. While Foreground holds for a session: Diagnostic probes and the gym’s trend work are deferred and Readiness probes drop to their slowest interval; activation of staged generations, garbage collection, store optimization, catalog and model updates, and sandbox snapshot creation are deferred; non-urgent notifications are queued and delivered as one batch when quiet mode ends; the model is not loaded (§20). Not deferred: repairs of units the foreground work plausibly depends on (audio, compositor, input devices, network, Bluetooth), including a repair rollback whose incident targets one of them (see Rollbacks), failure-posture responses, and repairs the user requests. Deferred repairs are deferred in fact: their incidents’ budgets and backoff do not advance while quiet mode holds (§13), and the repair loop skips them rather than running them. Gym case: a reference game workload runs while a garbage collection, a staged update, and a non-urgent fault are queued; frame-time p99 matches a baseline with Planarian stopped, and each deferred item runs within its window afterward; a PipeWire kill during the workload is repaired immediately and produces exactly one notice.

§12g. Sandbox tier (Phase B)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Agent-speed iteration happens beside the real machine, not on it. Three tiers, cheapest first: dry-run (verify_candidate, risk derivation, and closure diff, with no activation: seconds); sandbox (the candidate generation booted in a Firecracker microVM restored from a snapshot of the settled reference generation, probes run inside, VM discarded; snapshots keyed by generation hash; built with microvm.nix); real (this section’s transaction path). From B, a proposal at tier Tested or above reaches the real path only with a passing sandbox run against the same staged_source, recorded in the journal as evidence. The sandbox pass does not shorten the real settle window. Where KVM is unavailable, the daemon reports sandbox: unavailable and Tested is floored to AskFirst: the machine degrades to a person looking first, never to skipping the check. The sandbox’s verdict is evidence, like a probe; it never promotes anything, and it runs untrusted candidates under Firecracker’s jailer. Firecracker cannot host device-family faults (GPU, Wi-Fi hardware, USB, suspend/resume); those stay on the QEMU or bare-metal gym target.

§12h. Time

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Everything that does not survive a reboot is timed on CLOCK_BOOTTIME, never wall-clock time: approval-token expiry (tokens are not carried across daemon restarts at all, §12 step 4; the checkpoint keeps consumed nonces only), lease deadlines, cooldowns, settle windows, activation windows, and the parking limit (§12f). Wall-clock time is used only for what is shown to people and for records that must be compared across boots. The posture options recommend authenticated time (NTS) and report unauthenticated NTP as a notice. Gym case: the wall clock is moved back 48 h after an approval is issued; the token still expires 24 h after issue.

§13. Incidents and the diagnoser (crates incident, diagnose)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Incident lifecycle

  • Open when a unit is expected (desired condition holds, no override) and its derived status is Failed, Degraded, Stopped-but-desired-Running, Detected-but-desired-Bound, or Absent-but-desired-present, with fresh evidence for two consecutive ticks.
  • Defer to systemd first. If the unit has Restart= and systemd is within its own StartLimitBurst, the incident waits for systemd’s attempt (up to the unit’s RestartSec plus settle) before Planarian acts, so two controllers don’t restart the same service.
  • Attempts are transactions; each outcome is appended. Budget 3 per incident, persisted in the journal (survives daemon restart). Backoff between attempts: 10 s, 60 s, 300 s.
  • Closed when the target satisfies its desired predicate (Running, Bound, Stopped, or Absent as desired) with fresh evidence for a stabilization window (5 min).
  • Regressed: if the same target diverges again within 30 min of Close, the incident reopens with count+1 and a longer backoff; at count = 3 it goes straight to Escalated (report to user; cooldown 1 h; no autonomous action on that target until a human acts or the cooldown ends).
  • Cooldown after budget exhaustion: 1 h, then one more attempt, then 6 h.
  • An override on the target closes the incident (I13).
  • A person acting on the target (release, override, stop, a manual repair) reopens the incident with a fresh attempt and no cooldown — but only when the action was authorized and succeeded. A request that is refused, parked, or fails leaves the incident record exactly as it was: budget, cooldown, escalation and next attempt unchanged. Rejection grants no indirect control over the repair loop. Gym case: an unauthorized planarian release sshd during sshd’s cooldown changes nothing about the incident.
  • Quiet mode. While Foreground holds, incidents on units the foreground work does not depend on wait (§12f); their budgets and backoff do not advance while they wait.

Diagnoser

  1. From the open incidents, walk Requires edges (and Diagnostic edges with confidence ≥ 0.7) to the deepest divergent unit; that is the target. Ordering and Wants edges are never followed: a failed wanted unit is its own incident and is never assumed to be the cause of the wanting unit’s failure. Cycles: the walked subgraph is first collapsed into strongly connected components; “deepest” is defined on the resulting DAG; within a component, the target is the unit with the earliest definite Observed::Failed timestamp, else the one with the most failed Readiness probes, else lexical order. Gym case: an optional wanted service fails while its wanting service stays healthy (two incidents at most, no false root cause).
  2. Enumerate candidate transitions, depth ≤ 2, whose precondition holds and whose prediction closes the divergence. Exclude exhausted and cooling targets.
  3. Order by: tier ascending, then disruption ascending, then recovery (Idempotent < Revertible), then cost. Rollback last.
  4. Submit one at a time as Proposal { source: Diagnoser }. Stop at the first Committed whose expectation held.
  5. Exhausted → DiagnoserExhausted → model (B) or user report (A).

Phase A ApplyChange candidates are limited to catalog entries tagged repair: (re-enable a predicted option; reapply a previously-committed change for the same target — the latter is in the catalog from v4.6; it was specified and not shipped in v4.5).

§14. Intent matcher (crate intent)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Deterministic; no language model; identical behavior in degraded mode. Not in the Verus subset.

  • Index: every UnitDef.synonyms, every transition verb with a phrasing list, every change-catalog entry’s description tokens, every runtime-settable option.
  • Match: normalized token overlap plus a fixed pattern set (“restart X”, “reconnect X”, “turn X on/off”, “make X bigger/smaller”, “stop/pause X [for N minutes|until I say]”, “start X again”/“done with X”). Output (Transition, confidence).
  • Thresholds: ≥ 0.8 propose with Source::Intent; 0.5–0.8 ask the user to confirm one of the top two; < 0.5 unmatched (A: say so; B: summon the model).
  • Stopping: “stop X” and its variants map to StopAndSuppress { until: parsed or Explicit }, never to a bare StopUnit or a bare SetOverride; the override is journaled before the stop (§12), so there is no window in which the repair loop restarts the unit. “Turn wifi off” and similar map to an override on the network unit. “Start X again”/“done with X” → LiftOverride, after which the unit is desired-Running again and is repaired if needed. A StopUnit issued through the CLI by an authorized user is rewritten to StopAndSuppress { until: NextBoot } with a notice. The first time a user’s raw stop is caught as the unattributed residue (§11), the notice teaches the attributed form.
  • Authorization applies as in §11; an unauthorized match is reported as such, not silently dropped.

§15. Gym (crate gym)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • Catalog entry: Fault { id, family, inject: Transition | RawAction, required_post: Expectation, intent: Option<Intent>, tags }. intent marks cases where the correct behavior is no intervention (user stopped the service; login screen idle; probe timed out but the service is fine).
  • Phase A catalog, minimum families: service killed; service misconfigured; required dependency stopped; ordering-only predecessor stopped (must NOT be treated as cause); driver unbound; bad generation activated; daemon killed during Activating, Checking, and Reverting; revert fails (RecoveryFailed path); probe delayed past timeout with the service healthy; external config edit during a build; intentional service stop; suspend/resume; network unavailable at the gateway with the driver fine; flapping service (fails 30 s after each restart); storage full; manifest edited to weaken a probe; login screen idle with autologin off; boot-to-desktop failure.
  • v4-review acceptance cases (all required in Phase A): kill and immediately restart the daemon while a deliberately slow activation worker survives (activation and revert never overlap; the old owner cannot commit); an override-only candidate changes the evaluated output; the owner edits the source during a build (transaction rejected Stale); a crash injected at each of Activating, Checking, Published, Promoted, Reverting (running state, boot selection, override source, and journal agree after recovery); disable then re-enable a service with a canonical Readiness probe (no revert of the authorized change); Diagnostic evidence expires on a healthy machine (status stays Running); a definite failed service while another unit’s probe is stale (status Failed, not Unknown); authentication succeeds but the compositor never reaches its target (incident opens); idle greeter with autologin off (no intervention); a user stops a running service (stays stopped, no repair race); an unprivileged user tries to override a system Security unit (held for administrator approval; the unit stays up); an unprivileged user tries to stop a system unit that is neither Security-sensitive nor user-controllable (rejected Unauthorized); the daemon proposes a session-dropping driver rebind and a runtime Wi-Fi change (both AskFirst); an optional Wants= unit fails while its wanting unit stays healthy (no false root cause); the journal filesystem is filled to the reserved-block boundary as the daemon’s uid (emergency collection recorded and executed, or explicit NoSpace/RecoveryFailed).
  • v4.4 acceptance cases (all required in Phase A): for each failure class under the Standard posture, inject the failure (mask the unit; kill it past its budget; corrupt the core hash; break the journal chain; plant an undeclared early unit) and verify exactly the declared responses, that no agent principal started, and that the recovery exit works (the other postures from C); a security update followed by an induced probe failure (revert does not cross the floor; the incident escalates with RevertBlockedByFloor); a kernel update (takes the boot-only path; confirmed at the next boot, and reverted when the injected boot fails); a user-session principal overrides the firewall (held for administrator approval; firewall stays up); a non-administrator stops the network while another uid’s session is online (AskFirst; the other session is notified); two graphical sessions for one uid where the second compositor fails (an incident opens for the second session only); logging out of one of two sessions (user services keep running); an unattributed kill -9 of a service under Standard (accepted until next boot with attribution: Inferred and an undo), of the firewall (restarted and alarmed), and an OOM kill (repaired as a fault); a retained journal record edited on disk (detected on read as an Integrity failure); a staged update while Foreground holds (not activated until the window); the fake backend run against every docs/assumptions.md entry under injected Partial, Unknown, and fenced outcomes.
  • v4.6 acceptance cases (all required in Phase A; each closes a finding §28 names): agent-name-collision (alice’s running agent stays unauthorized for a system unit after an administrator re-runs agent run with the same name; the second start is refused naming the owner); agents launched through the API, then Absent, Crashed and Integrity injected (every launched agent stops; new starts fail; a per-uid launch over the limit is refused); a denied release during a cooldown (the incident record is unchanged); SetOverride(Stopped) and StopAndSuppress on display-manager from a remote administrator (identical disruption and floor); a rollback between vetted generations with equal manifests and a changed network unit (ConnectivityLoss scheduling); a fixing generation committed via daemon, via watchdog and via boot-only (the floor moves in all three); a crash between OpBegin(publish_boot_once) and OpEnd (after restart and reboot, boot selection, roots, overrides and journal agree); a boot-only candidate admitted but failing the 10-minute confirmation (Reverted, default restored, notice); verify_candidate on a store path whose NAR hash differs from the recorded one (refused); an approval issued, the daemon restarted, the change re-requests approval; a transaction re-prepared after restart keeps its original caller (own-session exemption still applies); an unauthorized caller’s ApplyChange (zero stage/evaluate/build calls); a checkpoint with a bad CRC (Integrity, not Absent); planarian-recovery crash-looping (Crashed raised); a generation that lowers the posture switched live (the booted posture stays in force until reboot); a unit Degraded before a change that loses a second probe (Checking fails); a dry activation that exits nonzero, times out, and emits truncated output (no autonomous activation in any case); a stalled resolver and a trickling HTTP endpoint (the batch returns by the deadline; the repair loop ticks during it); an owner edit while a change waits for an idle session (Stale when the window opens); SetOverride over another principal’s attested override (Rejected(Precondition)); an approval, then planarian log names the approver; a transaction parked past the limit (Cancelled(Daemon) with a notice); an SSH-only login (no graphical incident); a manifest with a 0.5-confidence edge to an external dependency (rejected at evaluation); a user’s first raw kill caught as the residue (the notice names planarian stop <unit>); a Revert to an older generation (no floor set); a §12a write-back issued while a generation transaction is active (queued, then committed, no notice); a crash during build (the orphan root is gone after the age limit); rollback-repair-foreground-dependency, rollback-repair-foreground-unrelated, rollback-repair-session-loss-asks (§12f Rollbacks).
  • Harness contract. The runners report what the catalog asked, never what they managed to run: a runner (fake, VM, by-hand run-vm.sh) exits nonzero on any FAIL, PARTIAL or ERROR and on an UNSUPPORTED verdict for a must-pass or acceptance entry that has no waiver in docs/deviations.md naming that entry and harness. CI runs the fake catalog with --strict. A waiver is a reviewed row, not a default.
  • Fault families also include a malicious declaration: a declared unit with a known-bad payload; Planarian cannot tell it from a legitimate service, so the case scores whether the incident report surfaces the declaration’s origin (who declared it, when, through which path).
  • Scoring per fault: pass iff required_post holds (or, for intent cases, iff no autonomous transaction was opened after the user’s own setup action; the user’s authorized StopAndSuppress or similar is the setup, not a failure). Gamed success (probe tampering, StopUnit(target), an override whose by is the daemon, the watchdog or the model, or whose log record is attributed to an agent) fails the run. An Unattributed inferred override is legitimate only in the catalog cases that declare it as the expected outcome.
  • Test builds. Fault injection in the real binaries lives behind the gym-hooks feature, in a separately named package; it is never installed outside the gym’s VM tests (§12d).
  • Metrics reported: repair rate; unnecessary interventions (transactions on intent cases); disruption incurred (sum of Disruption of executed transitions); time-to-repair; restart count per target; RecoveryFailed count (must be 0 unless injected); the invisibility budget (§21: idle CPU and memory are measured over the service cgroups of planariad and planarian-recovery, so spawned helpers count; the frame-time figure is a real p99 over frame samples, and a stand-in metric is reported as a stand-in, never as the budget); AskFirst prompts per user-week; catalog coverage per profile (§9); the verification ratio, the trust count, and the internal-unproven count (§17a); open bypasses by severity (§17b). The invisibility budget and the verification numbers are ratchets: every gate requires that they not degrade. Every gate requires zero open High bypasses.
  • Phase B: fault families are split into train/held-out; GB is measured on held-out families.
  • Targets: NixOS 26.05 VM gates. Non-gating from Phase A: an Asterinas NixOS VM and a Firecracker microVM (from a snapshot of the settled reference generation; service, configuration, transaction, and intent families only), failures classed Driver/Backend/Core; only Core failures are bugs. The Firecracker target becomes gating for the families it can host in B.

§16. CLI and console

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

CLI: planarian status | log | incidents | approve <hash> | resolve <txn> --as-committed|--as-reverted|--manual-repaired | undo | mark-good | mark-security-floor | override <unit> [--leave-alone|--stopped|--absent|--detached] [--until] | release <unit> | stop <unit> [--until] | ack | report boot | posture | hold release <unit> [--until] | agent run <name> -- <command> | gc, and ask "<phrase>". agent run starts an agent with the caller’s permissions, attributed to the agent (§11); it refuses a name already in use (I24). override/stop/release create and lift Overrides with the caller’s identity. ack acknowledges a failure-posture alarm (administrator for Integrity) and, with a unit, an inferred override (§11). report boot prints the last boot report; posture prints the declared posture and timeout and whether a weaker declaration is waiting for the next boot (§12d). Every verb §16 names exists in A; changing the posture or timeout is a configuration change (§12d). approve and mark-good require root and an interactive TTY in Phase A, the TTY established by the daemon from the peer for both verbs (§11; known weakness: any root process with a terminal qualifies; broker in C). mark-good changes the recovery baseline and requires administrator authority. known_good is always boot-confirmed (I1); mark-good on a boot-confirmed generation moves known_good to it. mark-good --accept-unconfirmed instead sets a separate accepted_baseline, which the revert path may use as revert_to when it is newer than known_good, but which never replaces known_good in the boot menu until a reboot confirms it. Both are refused if any Readiness probe is failing. Initial enrollment: the first generation booted after install becomes known_good once its Readiness probes pass at the 10-minute checkpoint on that boot; until then Planarian runs with max_tier = Runtime and reports “no baseline yet.”

Approvals show the change (owner decision, Nov 2). For a generation transition (ApplyChange, SwitchGeneration, Revert, Rollback) that asks, planarian approve, the console and the notice show the configuration diff it would apply: each option added, removed or changed, with its old and new value and its sensitivity, the Security- and External-sensitive ones first, and the units it would remove or stop. The same diff is part of the pending approval as structured data (planarian approve --json; IPC PendingApproval.diff), so an agent reads exactly what a person sees. The core computes it from the evaluated generations, never from the proposal; if it differs when the facts are re-derived at approval, the approval does not apply: the change is refused as stale, or asked for again when it re-binds.

Operational limits (normative): attempt budget 3 per incident; backoff 10 s / 60 s / 300 s; stabilization 5 min; regression window 30 min; escalation at 3 regressions; cooldowns 1 h then 6 h; token expiry 24 h; tick 5 s; probe timeouts as declared, default 10 s; settle default 15 s; generation deadline 15 min (networking 90 s); watchdog period 10 s; job-drain wait 60 s; journal preallocation 64 MB, compaction at 75%.

§16a. Visual console (read-only in A)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Socket limits. Connections are bounded per uid and in total; a share of the total is reserved for administrators and for callers in a logind session, so services with dynamic uids cannot exhaust it. Work the socket can cause before a proposal is authorized (§12 step 2) is bounded the same way: one build slot per caller class, charged to the caller.

Frontends. Planarian exposes one interface, its socket API. The CLI is always present. The console is one optional frontend, shipped in the personal profile; any other frontend (a web dashboard, a TUI, an agent harness, a fleet console) uses the same API and gets the same identity derivation and policy checks. No frontend is privileged.

iced; COSMIC reference desktop; autostart as a user service on graphical-session.target, minimized unless there is an open incident, a pending approval, a reconciliation record, or the last boot reverted. Views: Graph (desired vs observed vs health per node: three small indicators, not one color; edge style by kind; overridden nodes hatched; Unknown nodes grey with a stale-clock mark), Timeline (transactions with journal states and outcomes), Generations (current, previous, known_good, live_tested, candidate; boot-confirmed marker), Incidents (state, attempts, cooldown). Buttons in A: undo, override/release (Runtime, user-actor). No approval in A. Reads only the daemon socket.

§17. Pinned versions and conventions

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

NixOS 26.05 (nixos-26.05 channel pinned to a commit in flake.lock; recorded in docs/pins.md; moves only by PR; 26.11 waits for Phase C). COSMIC from the channel module. Rust stable pinned. zbus, serde+JSON. No unsafe outside backends/ and watchdog/. License MPL-2.0. CI: tests, gym Phase A catalog, dependency-boundary check, Verus proofs, Kani harnesses.

§17a. Verification plan and trusted assumptions

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Proof targets, in order: Phase A — schema::digest; policy::decide (purity; I5; I11; I14 in the sense that decide has no Proposal input; probe-tamper → AskFirst or Reject; I16 in decide and revert-target selection; I19: no token whose approver is the proposer, no self-grant); txn kernel (I6, including Built-waiting and AwaitingBoot; I12: every journaled transaction reaches a terminal state under the modeled outcomes; write-ahead ordering). Added 2026-10-29: the journal’s hash chain (I18: every record written commits to its predecessor’s hash, and the check on read reports the first break), in txn under Verus in A, trusting only SHA-256; the residue classifier (§11: which unit endings are a person’s stop and which are faults) extracted as a pure function with Kani harnesses in A, Verus with exec in C; I17 (user_authorization gives root the administrator capability, so the set is never empty while root exists) in policy in A. exec orchestration gets Kani harnesses and proptest in A, Verus in C, together with I13 (the incident planner never targets an overridden unit). Diagnoser soundness and scorer in B. Broker in C. Switch/rollback and supervisor in D (store integrity proven only for content-addressed paths; input-addressed paths are verified by signature and NAR hash, not by content). OSTD, model channel, core update path in E.

docs/assumptions.md lists every externally specified behavior the proofs rely on, maintained as Verus external_body/assume sites are added. Phase A entries: backend Outcome is honest (Succeeded means applied; Unknown means the backend cannot say); activate_live(revert_to) restores the activation state of revert_to; publish_boot and the promotion rename are idempotent; the journal’s fsync is durable and the preallocation is writable; systemd reports unit state truthfully over D-Bus and honors X-RestartIfChanged/X-StopIfChanged; fencing is effective (a stale-epoch call is refused before it mutates); the kernel and systemd are correct. Re-observation after a backend call detects divergence from the assumed postcondition; it does not undo an irreversible effect, which is why the NonReversible floor exists.

Progress assumptions for I12 (liveness is conditional on these, stated in the proof): some lease holder (daemon or watchdog) eventually runs; a fenced worker eventually stops issuing calls or its calls are refused; journal writes eventually succeed (guaranteed by the preallocation for recovery records); backend calls terminate. Under those, every transaction reaches a terminal state; with them violated the proof says nothing, and RecoveryFailed plus alerting is the designed fallback.

Verification ratio. The trusted computing base is 𝒱 ∪ 𝒯: 𝒱, the proven base, is what is verified (schema, policy, txn, and later crates as they enter the Verus subset); 𝒯, the trusted base, is what is trusted but unproven (backends, Nix, systemd, the kernel, firmware), with every trust recorded in docs/assumptions.md. 𝒯 is accounted in three parts. Platform trust (P, G): what the machine underneath does. Proof-validity trust (V, plus the specifications the proofs prove against, Verus, Z3, and rustc): what makes a proof mean what it says. Internal unproven code (I-ids: exec::derive, user_authorization and the admin lookup, the residue classifier, journal I/O, the identity resolution of §10): Planarian’s own code that the proven gates take input from. Internal unproven code is not an assumption about the world; it is work not yet proved, so it is reported as its own number, the internal-unproven count, never folded into the trust count, and it must not grow and is expected to fall from C. Every proved invariant also has at least one gym entry written from the spec’s English rather than from its formal specification, since a proof certifies a wrong specification as readily as a right one. Every gate reports three numbers: the trust count (the count of platform and proof-validity entries in assumptions.md, including external_body and assume sites), the internal-unproven count, and the ratio d:a, discharged (d) to assumed (a) obligations, per invariant (“I12 rests on 3 discharged lemmas and 5 assumptions”) and summed globally. GA reports both as the baseline. Every later gate requires that neither the global ratio nor any per-invariant ratio degrades and that the trust count does not grow. The v4.5 re-accounting (proof-validity entries added, internal code moved out of the trust count) sets a new baseline once, recorded with its reason; it is not a regression. D and E are expected to raise the ratio substantially; the gate requires only that it not fall, so that no one improves it with proofs that discharge nothing load-bearing.

The accounting is itself checked. planarian-gym verification rejects a duplicate obligation id; requires every invariant in the baseline to be present in the current accounting (a removed invariant is a failure, not an absence); and ties each discharged obligation to a proof artifact the verifier actually produced (the Verus check’s output, or a reviewed manifest naming the proof function), not to the textual presence of a function name. Gym mutations: delete an invariant, duplicate an obligation, rename a proof function; each fails the gate.

Status of the Phase A proof targets at v4.6. Proved: policy::decide (I5, I11, I14-decide, I16-decide, I19, I21, I22), I17 (policy::i17_administrators_never_empty), the txn kernel (I6, I12 conditional, I15, I23, write-ahead ordering), the hash chain (I18). Checked by Kani harnesses: the residue classifier (a Security unit’s stop is never inferred as intended, a crash is always a fault, residue is inferred only where the posture allows it; crates/exec/src/kani.rs). Property-tested, not proved (deviations): schema::digest. Every target is counted in docs/verification-ratio.toml, and CI fails when a count differs.

§17b. Bypass register

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

docs/bypasses.md lists every known route by which the machine can reach a state this spec says cannot happen, or by which a stated guarantee stops describing the real machine, whether or not a proof is involved. One row per route: id (B for open, X for accepted; never reused), route, what it breaks (always invoked, tamperproof, or correct), severity (High: defeats a stated guarantee in the shipped configuration without root; Medium: needs root, a network position, or a likely deployment, or the spec overstates the mechanism; Low: narrow, availability-only, or process; Accepted: outside the claim by design), status, mitigation, phase, and the invariants it limits (all, or — for none; §17c). Any change that adds a path to the machine (a request type, backend call, hook, or posture option) adds a row or states in its PR that it opens none. planarian-gym verification --gate checks the file’s format and fails a gate with an open High row. The file has three tables — Open, Closed (status Closed (<sha>), kept forever), and Accepted — and a row moves between them when its status changes; the format check rejects a Closed row in the Open table. Rows added at v4.6: B-20 (High, Open: agent-name collision rebinds a running agent’s authority; closed by I24); X-12 (Accepted: an override-file writer that does not take the daemon’s lock).

Verus subset applies to schema, policy, txn only. exec, diagnose, incident, state keep the no-dyn and no-unsafe rules so they can be brought in later; intent, console, daemon, cli, gym are unconstrained.

§17c. Guarantees site

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Planarian publishes a generated documentation site organized around its guarantees, as docs.rs is organized around a crate’s API: every page is computed from the repository at one commit, never written by hand, and every claim links down to the code, proofs, tests and limits that carry it. It exists so an auditor can start from “what does this system promise?” and reach the exact lines that keep the promise, at the exact commit under audit.

Sources. The site is built only from files the repository already maintains: the invariant table (§7), docs/verification-ratio.toml (obligations and assumptions per invariant), docs/assumptions.md, docs/bypasses.md, the gym catalog (each entry’s spec and invariants fields) and its latest run results, docs/spec-history.md, and the source tree (to locate each obligation by name). Nothing on the site is entered anywhere else.

Pages.

  • One page per invariant: the statement verbatim from §7; proof phase, status and d:a against the baseline; each discharged obligation with its kind (verified fn, proof fn, Kani harness), what it guarantees, and a permalink to its line at the commit; the assumptions it rests on, each with its text, its kind (platform, proof-validity, internal unproven) and the other invariants that rest on it; the global proof-validity assumptions that apply to every proof; the gym entries that exercise it, with harness, expectation and the verdict from the run at that commit; the bypass rows that limit it; and its history across spec versions.
  • Reverse indexes: per assumption (which invariants fall if it is false); per bypass (which guarantees it narrows); per source file (which invariants and obligations live there); per gym entry (what it exercises).
  • The spec itself, one page per section, with cross-references as links.
  • API docs: cargo doc output for the workspace, linked from invariant pages for the types they concern. Proof obligations link to source lines, not rustdoc, because rustdoc renders verus! blocks poorly.

Versions. A snapshot is built for every tagged release and for the head of main. Tagged snapshots are immutable and kept; main is rebuilt on every push. Every page names its commit and spec version in its first line and links to the same page in other snapshots.

The build is the check. The generator fails the build on any reference it cannot resolve: an obligation name with no matching function, an assumption or bypass id with no entry, an invariant with no page, a gym entry naming an invariant that does not exist, a bypass row with no invariants column. CI runs it on every push, so a docs failure is a build failure; a gate claim (§18 rule 19) may cite the site’s snapshot at the gate commit.

Where it lives. The generator is a subcommand of planarian-gym (planarian-gym site), beside verification, which already parses these files and checks the same names. The site is an mdBook published to GitHub Pages from CI. It is generated output and is never committed to the repository.

§18. Instructions to the coding agent (AGENTS.md)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  1. First deliverable, before any other code: the Phase A fault catalog (crates/gym/catalog/phase-a/: 00-format.toml and one <family>.toml per §15 family, read in filename order as one document), one entry per §15 family at minimum, each with inject, required_post, intent, family; stop for owner review.
  2. Second deliverable: the GA demonstration script (gym/demo.sh): fault → repair; bad config → revert; kill planariad during Activating; watchdog reverts; restart daemon; reconciliation record printed. Build toward making it pass; everything else is in service of it.
  3. Implement Phase A only; traits for B–E with one implementation.
  4. Build order (historical; every crate exists at v4.6): schema → txn (with the journal and the fake backends) → policy → state + backends/linux (systemd, logind, sysfs) → exec + backends/linux (nixos-rebuild) → watchdog → changes → incident → diagnose → gym → intent → cli → daemon → console. The console Graph view may be built as soon as state works.
  5. Preserve however the schema evolves: I5, I6, I10–I25; digest() excludes resources and timestamps; Proposal is the only write path; policy facts derived by the executor; gym scores on required_post and intent cases, never on absence of errors; diagnoser before any model; probes with side effects other than None or a bounded Measurement never on a cadence.
  6. Do not resolve §19 items; TODO(owner) with the stated default.
  7. No language model, LLM API, or prompt anywhere in Phase A.
  8. Where NixOS and this spec disagree on mechanism, follow NixOS and record in docs/deviations.md.
  9. Every executor outcome must be reproducible from the log: proposal, pre/post state, DerivedFacts, PolicyVersion, probe evidence, decision, outcome.
  10. schema, policy, txn in the Verus subset; land policy proofs before starting exec.
  11. The Asterinas and Firecracker gym targets may stay red in A. Classify failures; fix only Core.
  12. Every new assume/external_body adds a line to docs/assumptions.md in the same PR.
  13. The fake backend used by the transaction kernel MUST satisfy every entry in docs/assumptions.md, not only the trait signatures: it is a second real implementation, not a mock. The gym runs the kernel against it under injected Partial, Unknown, and fenced outcomes.
  14. The journal is hash-chained from the first commit that writes it (I18).
  15. Never implement Planarian’s response to its own failure inside planariad; it lives in nix/posture.nix as systemd and initrd units (§12d).
  16. Report the invisibility budget (§21) and the verification ratio (§17a) from the first gym run; treat any degradation as a failing test.
  17. docs/future-work.md is non-normative. Cite it; do not implement from it. If anything there seems to block Phase A, stop and ask.
  18. Phase A was held open for v4.4: the schema-affecting changes (Override, the Actor shape, the security floor, the hash-chained journal) land before the Phase A proofs are written, not as migrations after them.
  19. Keep docs/bypasses.md current (§17b): a PR that adds a path to the machine adds a row or says it opens none.
  20. v4.6 handoff, in this order. (a) Reconcile: the close-out review in the project (“Phase A close-out review, head 805cd1c”) lists every finding with its status at that head; before touching anything, confirm each item you take on is still open at your head and record which commit closed the ones that aren’t. (b) B-20 first (I24; §11, §16): register the row, fix, add agent-name-collision to both harnesses. (c) The documentation sweep: every doc agrees with head and with this spec before any further status is cited (ga-status.md’s summary; guarantees.md’s accounting; bypasses.md’s three tables; AGENTS.md mirroring §18 rules 0–23; catalog-review.md’s counts and a v4.6 section; the catalog’s header (phase-a/00-format.toml); the deviations rows §28 names). (d) The §28 required changes, one PR each, each with its §15 acceptance case landing in the same PR. (e) §15’s harness contract and §17a’s accounting checks. (f) Then, and only then, run the whole VM catalog on one commit and update ga-status.md from that run; a gate claim cites one commit and one run, never a union of runs.
  21. Equivalent effects, equivalent treatment. When two spellings of a request, two paths to a commit, or two sources of a proposal produce the same effect on the machine, they get the same derived facts, the same floors, and the same bookkeeping (§11, I25). A PR that adds a spelling, a path or a source states which existing one it is equivalent to and shows the gym case that holds them equal.
  22. Nothing is derived from a failed tool. A backend or helper that exits nonzero, times out, or returns truncated output never contributes a smaller plan, a None disruption, or a passing verdict; the step fails or the change parks (§9 step 3, §15 harness contract).
  23. The guarantees site (§17c) builds on every push. A change that renames an obligation, adds an invariant or assumption, or adds a bypass row makes the site resolve in the same PR.
  24. Property tests for logic, regression tests for bugs (owner decision, Nov 3). A PR that changes a pure fold, parser, classifier, ordering or state machine carries a property test (proptest, or a Kani harness where one fits) stating the invariant it changes, not only an example input, and shows that the test fails without the change. A PR that fixes a bug adds a test that fails on the unfixed code and a row to docs/regression-ledger.md (bug, fix, test, kind, evidence it fails on revert); a bug that only a kernel or a VM can show cites its VM test there. Nix wiring, documentation and fixtures need none and say so.

§19. Open questions (owner decides)

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • GA threshold 60%; NixOS 26.05; MPL-2.0.
  • All of Future Work v0.2 §1 accepted into v4.4; schema-affecting items land in Phase A (Oct 28).
  • Invisibility budget targets: idle CPU < 0.5% of one core; resident memory < 50 MB excluding any model; no statistically significant frame-time or input-latency difference with Planarian running vs. stopped. Reference workload: one GPU-heavy game under Proton; the agent proposes the title and a scripted benchmark for owner sign-off.
  • Probe kinds: typed kinds by default; administrator-authored Exec probes allowed, flagged.
  • Hash-chained journal is a Phase A requirement.
  • Unattributed-stop residue: accepted with an undo under Relaxed/Standard; ask window under Guarded/Sealed.
  • Multi-user system-scope overrides: AskFirst for non-administrators when another uid’s session is online.
  • Sandbox pass required for Tested where KVM exists, floored to AskFirst where not; Firecracker gym target non-gating from A.
  • Installer selects the posture; boot-ready timeout configurable (60 s default, 15–600 s).
  • Phase B grants to non-human principals: catalog and model-catalog authority only; merge authority requires two distinct approvers.
  • Learned Diagnostic edges are never admitted without a passing gym case.
  • Spec vulnerability review (Oct 29) merged as v4.5: a launcher for agents; the floor from the whole closure; no request field decides authority; trust accounting in three parts; I2, I14, I16, I18 qualified; the bypass register as a gate.
  • Repairs run while an admitted boot-only change awaits its confirmation; a canonical repair in that window means it is not confirmed (Oct 31; I6, §10).
  • Guarantees site: generated per commit and per tag, organized around invariants, published to GitHub Pages; never hand-edited (Oct 31; §17c).
  • Rollbacks in activation scheduling: user-requested runs at once; diagnoser rollback after failed repair is a repair, floors and quiet mode still apply (Oct 31; §12f).
  • Agents act with their user’s permissions, attributed per instance (Oct 30; §11, I24). GC stays Runtime with retainAllGenerations (Oct 30; I21, §12b).
  • Close-out review (Oct 30) merged as v4.6. Decided: boot-only fallback is the publishing (running) generation, never known_good automatically (§12 step 7); parking in Built is bounded at 24 h, AwaitingBoot is unbounded and reported as waiting (§12f, I12); an external dependency needs a traversable edge, and the module rejects weaker ones (§10); approval tokens die with the daemon (§12 step 4, §12h). Taken as defaults, overturnable here: boot confirmation has two milestones, admission and the 10-minute confirmation (§10); only seated sessions carry a graphical expectation (§10); §6 describes the repository as it is.
  • assets/identity/source/prompts.md records that the raster master was generated with an image model. Keep the provenance note in the public repository, or remove it? And is the identity kit under the repository’s MPL-2.0 or reserved? (Not a spec matter; recorded here so it is decided.)
  • Rule 5 (TODO(owner) markers) has never been used: no marker exists in the tree. Either every default below was resolved (then say so in AGENTS.md and retire the rule) or the convention should start being followed.
  • Provisional, revisit at each review: inferStops (§11) stands in for a unit-importance property. Revisit if importance should also affect daemon-proposed stops, quiet-mode deferral, diagnoser ordering, or the console. If it should, add importance as its own field, not a Sensitivity: sensitivity is the risk of changing a unit, importance the harm of its absence.
  • Catalog coverage threshold for the personal-profile gate: set after the first catalog survey.
  • Choice of the reference game and benchmark sequence for the invisibility budget.
  • Phase B threshold Y.
  • Six observed states plus Unknown/Transitioning — revisit after the A catalog.
  • Phase C snapshot backend: btrfs only, or btrfs + ZFS?
  • Nix vs Nickel for the Phase D evaluator.
  • Boot model family/size (default ~4B, 4-bit, CPU, 8 GB floor).
  • Reference laptop (E). Fork Asterinas NixOS or parallel distribution (E).
  • Should Diagnostic edges be learnable from incident history in C, or stay manifest-only?

§20. Later phases — design notes carried forward

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Not Phase A work; kept so the design is not lost.

Model service (B). The model is treated as an adversary: its judgement is never trusted and is not a vector, because any proposal it can make is one an attacker with the same socket could make, and every proposal passes the same checks. What must hold is containment (I9). Until I9 is proved in E, it is held by the unit’s sandbox, which is normative: DynamicUser=; no capabilities; ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, read-only access to the model files and the report socket; RestrictAddressFamilies=AF_UNIX, widened only for a declared remote endpoint (future-work §3); a SystemCallFilter= allowlist; device access limited to the GPU’s render node when GPU inference is enabled; the proposal socket as its only write path. Gym case (B): from inside the service, attempt every other write path (the journal, the override file, the store, another unit’s socket, a capability-changing syscall); each fails and none leaves a trace outside the service. Unprivileged systemd service; reads reports over a Unix socket; emits Proposals. Boot model ~4B at 4-bit on CPU via mistral.rs or candle; RAM floor 8 GB. Residency is declared like any other unit: unit:model-service desired Resident when All[Not(Foreground), memory free ≥ model size + headroom], otherwise Unloaded. Loads lazily when an incident escalates to it (zero steady-state memory; 5–15 s cold start); unloads after a declared idle period (default 10 min) and immediately on GPU or memory pressure from any foreground principal: the user’s session always outranks agent inference. If an escalation arrives while loading is not allowed, it falls back in order to the deterministic path, CPU inference where available outside quiet mode, a configured remote endpoint (future-work §3) within its disclosure profile, or deferral until the condition clears. It never loads over a foreground app. keep_warm offered at ≥ 16 GB outside quiet mode. Summoned only on DiagnoserExhausted or intent < 0.5. Tools: inventory, health report (including attempts and the incident), dependency graph, config read, local manual search, ask-user, propose (cataloged changes or new catalog entries for human review; never raw Nix). All text inputs tagged untrusted. Training: distillation in the gym, scored on diagnoser-failed and held-out faults. Model planner: largest catalog model that fits measured resources; installed as a Tested generation; switched only if it beats the currently active model on repair accuracy; falls back to the boot model on resource loss. Catalog models hash-pinned with training manifest and eval results.

Approval broker and secure attention (C, E). Separate process minting ApprovalTokens for any principal holding approval capability (§11), never for its own proposals. C: its own socket and a confirmation surface the compositor cannot draw over. E: a kernel-intercepted secure attention key hands the display to the broker.

Remote principals, apps, fleets (future). The endpoint catalog and relay for remote agents, app capability manifests, fleet membership (Raft and BFT behind one interface, BFT the intended default), attestation, and gossip of shared knowledge are directions in docs/future-work.md §3–§5. The hooks in §9 (signed external desired state) and §10 (signed health report), and the reserved Actor::Agent, exist so they can arrive without reopening the proven crates.

Rust system layer (D). Snix (git.snix.dev/snix/snix; crates GPL-3.0, protobuf definitions MIT) as the GenerationBackend, spoken to over its MIT protocols from a separate process. Rust PID 1 plus supervisor as UnitBackend, driven by Requires edges with Wants preserved as weak. Rust CoW filesystem as SnapshotBackend. relibc for Rust std (vendors openlibm, C; a named exception). Store integrity is proven for content-addressed paths and signature-and-NAR-hash-checked for input-addressed ones. Backend contracts are revised at the start of D from docs/assumptions.md and docs/deviations.md.

Asterinas (E). Fork at a pinned release (0.19.0 at writing). Driver priority for a reference laptop: NVMe, input, display → Wi-Fi, Ethernet, USB → audio, power, suspend → GPU modesetting → GPU 3D/compute (port Nova or Tyr) → Bluetooth, webcam, fingerprint. Firmware blobs allowed. Desktop: Smithay compositor with Rust software renderer; iced UI; COSMIC later. Verification: OSTD (vostd), executor, policy, diagnoser, broker, switch/rollback, store integrity, model channel, core update path. Core image: signed bundle installed only with project signature plus secure attention; previous core stays bootable.

Trust base accepted as unproven at every phase: firmware, bootloader, Rust compiler, CPU; plus the Linux kernel and systemd in A–D.

§21. Expected resource profile

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

SituationCost
IdleSingle-digit MB resident, < 0.5% of one core (the invisibility budget below); watchdog sleeps on a 10 s timer
Invisibility budget (normative, ratchet)Idle CPU < 0.5% of one core; resident memory < 50 MB excluding any model; wakeups per second at idle reported; input-to-display latency and p99 frame time on the reference game workload with no statistically significant difference vs. Planarian stopped; notifications per user-week reported, and zero for self-resolved faults the user could not perceive
Fault → detectionOne tick plus probe timeout (5–30 s); up to one cadence period for hardware probes
Fault → deterministic fix10–60 s per attempt plus settle; defer-to-systemd adds up to RestartSec + settle
Fault → model-assisted fix (B)1–3 min, plus cold start if not warm
Config change (Tested)30 s to several minutes, dominated by Nix evaluation and builds; plus the stage copy (seconds)
Runtime-settable optionImmediate; write-back follows in the background
JournalOne fsync per state change, roughly ten per generation transaction; 64 MB preallocated
DiskBounded by keep-N and space budgets; BudgetUnsatisfiable reported if the protected set alone exceeds them
Executor logTens of KB/day in steady state; rotated at 50 MB, kept 30 days

§22. Changes from v3

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • Restored principles (now 12) and invariants (I1–I14) that v3 referenced but omitted.
  • Desired/observed/health separated; conditions (AfterLogin, DeviceConnected, power); suppressions; Unknown, Transitioning, stale evidence; user-scoped units. Fixes the login-screen and user-stopped-service faults.
  • Edges typed (Ordering/Requires/Diagnostic); After= no longer implies dependency.
  • Probe contract: sandbox, limits, side effects; disruptive probes OnDemand only; establishes field.
  • Sensitivity composable; Disruption and Recovery as separate properties; Decision::Reject separate from tiers; max_tier can no longer make a forbidden action approvable.
  • Policy facts derived by the executor from the evaluated generation (I14); OptionChange.sensitivity removed from proposals; refinement check and executor-owned requirements made explicit.
  • Approval bound to the concrete candidate, base, policy version, cumulative hash, derived facts; re-validated at execution; single-use.
  • Transaction kernel with write-ahead journal, Partial/Unknown outcomes, recovery watchdog as a separate unit, daemon restart reconciliation, self-protection during activation; live_tested vs boot-confirmed known_good.
  • Change catalog with handlers replaces free-form ConfigDiff in proposals; override file promoted only on commit; user config pinned per transaction.
  • Incident lifecycle: defer to systemd, backoff, durable budgets, regression and flap handling, cooldowns, escalation.
  • Gym: lifecycle and intent families; unnecessary-intervention and disruption metrics; held-out families for B; GA includes the kill-mid-activation demonstration.
  • Verification narrowed to schema/policy/txn with docs/assumptions.md; re-observation claim corrected; store-integrity claim corrected for input-addressed paths.
  • GC: in-flight candidates protected; space budgets.
  • mark-good authorization; log record contents; dyn/generics contradiction resolved.
  • Phase A scope kept full and Asterinas target kept non-gating, per owner decision.

§22–27. Earlier changes

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

The changes from v3 through v4.4 (§22–§27) are in docs/spec-history.md, with their section numbers unchanged. Only the latest revision’s changes stay in this file.

§23. Changes from v4

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • §12: exclusive transaction ownership with lease epochs; worker fencing via pidfd and job draining; the watchdog may not commit before Published; the daemon waits for a watchdog-held lease; I15 added; gym case for overlapping owners.
  • §9/§12: immutable staged source (NAR-hashed) evaluated with pure-eval; build before approval; verify_candidate before activation; the token binds expectation, revert target, and recovery strategy; activate_live and publish_boot separated; Published/Promoted journaled; atomic-rename-plus-dir-fsync promotion; watchdog revert restores the override source; crash-recovery table.
  • §8/§9/§10: ProbeRole Readiness vs Diagnostic; side-effect probes are Diagnostic; status derivation puts definite Failed above Unknown; probe applicability follows the candidate’s authorized desired state, distinct from definition tampering.
  • §8/§9: Condition::SessionEstablished from logind replaces the circular AfterLogin; the session probe is the result, not the prerequisite.
  • §8/§12/§14: StopAndSuppress composite; suppression journaled before stop; no post-hoc prompt; CLI StopUnit rewritten.
  • §11: SO_PEERCRED authentication; authorization rule separate from risk; actor fields overwritten; global floors for NonReversible and disruption apply to every transition kind.
  • §8/§9/§13: Wants edge kind preserved as weak; SCC-based root-causing for cycles.
  • §10/§12c: per-filesystem storage units; preallocated journal; ENOSPC behavior by state; CollectGarbage { emergency }; BudgetUnsatisfiable.
  • §12: read-only vs mutating backend calls; Prepared on the Allow path; per-transition RecoveryStrategy; Runtime outcomes never trigger generation reversion; restartIfChanged/stopIfChanged as the real self-protection mechanism.
  • §16: accepted_baseline distinct from boot-confirmed known_good; initial enrollment; operational limits made normative.
  • §17a: progress assumptions stated for I12; fencing and idempotence added to the trusted-assumptions list.
  • §12a, §14, §20, §21 made self-contained.

§24. Changes from v4.1

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • §12: takeover requires confirmed quiescence (jobs cancelled and gone, workers reaped); a drain timeout ends in RecoveryFailed, never in proceeding; lease transfer is atomic via flock plus rename; the watchdog triggers on Published/Promoted too and, from Checking, completes the whole authorized commit path or reverts.
  • §8/§12: per-transition Handler and RecoveryStrategy independent of tier; Rollback is a generation operation even at Runtime tier; BindDriver is a device operation with UnbindDevice recovery; the crash-recovery table branches on strategy.
  • §9/§12b: transaction-specific base.nix with content-hash identities; promotion and restoration idempotent by hash comparison; no shared .prev; GC root created at Built and kept through approval; dirty-Git staging defined by Nix-visible bytes.
  • §12c: length-prefixed CRC’d records with torn-tail handling; checkpoint-then-truncate compaction preserving all live state; filesystem contract for ext4/XFS/btrfs/ZFS with a sentinel fallback.
  • §8/§10: SideEffects::Measurement permits bounded network/local I/O for Readiness probes; RemoteStateChange stays Diagnostic; external dependencies are units with edges.

§25. Changes from v4.2

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

  • §9: base_source (owner source with base override) separated from staged_source (candidate tree); stale check compares the working tree to base_source only; both bound to the token; Git-revision wording removed; step numbering fixed.
  • §9/§12: build roots its own output under gcroots/planarian/builds/ without a lease; the transaction adopts the root; orphan roots cleaned at startup.
  • §12: durable OpId journaled before every mutating call and tagged onto the backend job; quiescence covers jobs discovered by jobs_tagged for unacknowledged ops; a backend that cannot say → RecoveryFailed.
  • §12/§12c: RecoveryFailed sets a persisted recovery interlock carried in the checkpoint; cleared only by administrator planarian resolve; crash table row for Checking, ReobserveOnly.
  • Consistency: ProbeRole, canonical-probe paragraph, and agent instruction 4 all carry the Measurement exception; I12’s terminal set includes Rejected and Cancelled.
  • I4 reworded to applicable Readiness probes; atomic transaction creation with initial lease; Rejected/Cancelled terminal states; incident closure on the desired predicate; SessionEstablished scoped to user-class sessions and a specific session id; intent-case scoring scoped to autonomous action.

§26. Changes from v4.3

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Incorporates the owner-approved conceptual review (Future Work v0.2 §1.1–§1.11 and the decisions in its §13).

  • §7: principles 13–15 (reference monitor and species-blind principals; invisible under normal use; failure response enforced outside the monitor); principle 9 generalized to attributed overrides. Invariants I16 (security floor), I17 (administrator set non-empty), I18 (hash-chained journal), I19 (no self-approval, no self-escalation), I20 (no agent without the monitor).
  • §8: Suppression generalized to Override { desired: LeaveAlone | Stopped | Absent | Detected, attribution }; Suppress/Unsuppress become SetOverride/LiftOverride; UnitStatus::Overridden. Actor::User carries session, seat, and active; Actor::Agent reserved. Condition::Foreground. security_floor in SystemState; below_security_floor and other_sessions_affected in DerivedFacts. Typed probe kinds DBus, FilePresent; Exec administrator-only. Posture, FailureClass. RejectReason gains the Unauthorized, Interrupted, and NoSpace variants v4.3 used but did not declare.
  • §9: probe-kind rule; signed external desired-state source (hook); catalog coverage as a gym metric; NotInCatalog by phase; graphical-session readiness bound to the session’s compositor; learned edges need a gym case. New §9a: PlanarianOS as a distribution with three profiles, per-profile gates, accounts, live mode.
  • §10: foreground signals; user services desired while any session of the uid exists; actor resolution from logind; boot report; signed health report (hook) with a core-computed disclosure profile.
  • §11: principals and capabilities; Phase B non-human grants; overrides on Security units need an administrator; per-session “own session”; multi-user and security-floor floors; approval as a capability with no self-approval; deliberate actions carry identity; the unattributed residue by posture.
  • §12: activation window and sandbox check on the generation path. New §12d (boot ordering, planarian-ready.target, failure classes, responses, four postures, configurable timeout, boot-chain locking), §12e (security floor), §12f (activation scheduling and quiet mode), §12g (sandbox tier, Phase B). §12c: hash-chained records and chain head in the checkpoint.
  • §13–§14: overrides; quiet-mode deferral; attributed stop phrasings.
  • §15: v4.4 acceptance cases; malicious-declaration family; invisibility, coverage, AskFirst-rate, and verification metrics as ratchets; Firecracker target non-gating from A.
  • §16: CLI additions; frontends section (the console is one optional frontend).
  • §17a: the verification ratio d:a and the trust count reported at every gate. Phase A proof targets add I16 (in decide and target selection) and I19.
  • §18: agent instructions 10–17; rule 4’s preserve list extended to I10–I20.
  • §4, §5, §6, §19, §20, §21: roadmap rows (Phase C is PlanarianOS), scope, layout, decisions, model residency, broker and future directions, invisibility budget. §20’s idle unload stays at 10 min, now declared rather than fixed.
  • §1–§3: overview prose, the diagram, and feature-table rows updated to overrides, attributed actions, the security floor, failure posture, quiet mode, and the sandbox tier.
  • Consistency pass after review: Decision::Allow carries an ApproverClass; ApprovalToken binds its approver; PrincipalId, Capability, Grant, and Authorization declared; Actor::Unattributed for inferred overrides, which never apply to Security-sensitive units; EscalationReason (including RevertBlockedByFloor); a boot-only generation path with the AwaitingBoot state; I1 and I6 clarified against the floor and waiting transactions; the v4-review Security-override case changed from rejected to held for administrator approval; CLI verbs override/release replace suppress/unsuppress; §12c’s checkpoint also carries overrides and the security floor.

§27. Changes from v4.4

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Incorporates the spec vulnerability review of Oct 29 (findings B-1–B-18 against commit 1bc09b2).

  • §5, §6, §7, §17b, §18: the bypass register docs/bypasses.md; principle 16 (name every way around); agent rule 18; no gate passes with an open High row (B-14 and the review’s appendix).
  • §7: principle 6 and I14 extended from proposals to every request field (B-3); I2 states what the A–D core hash detects (B-12); I16 states the inspected set (B-2); I18 states whom the chain holds against (B-8).
  • §10, §11: identity from a pidfd, never a bare pid (B-6); a caller property that matters is established by the daemon (B-3); agents are their own principals, started by planarian agent run with DynamicUser= (B-1); approval’s terminal established by the daemon (B-3); inferStops = false exempts units from residue inference, and inferred overrides stay listed until acknowledged (B-11; the review’s Critical sensitivity became this manifest option, so no proved type changes); sensitivity completeness with fail-closed defaults and a gym entry (B-5).
  • §10: the boot report covers units in A, processes from C, and flags a test build (B-10, B-13).
  • §12c: chain head in the signed health report for off-machine witnessing (C), measured-boot anchor (E); the external source’s sequence number in the checkpoint (B-8, B-16).
  • §12d: alarms are local in A; a dead-man signal from C; the core hash covers every Planarian binary and script; a test build is an Integrity failure (B-9, B-12, B-13).
  • §12e: the floor is derived from the whole system closure, with a CVE feed from B; gym case for a service-package fix (B-2).
  • §12h (new): time on CLOCK_BOOTTIME for everything that does not survive a reboot; NTS recommended (B-7).
  • §20, I9: the model is treated as an adversary, so its judgement is not a vector; containment is, and the model service’s sandbox is now normative, with a gym case in B (B-18; the review’s accepted row X-9 withdrawn).
  • §11, §19: inferStops marked provisional, with the condition for replacing it by an importance property.
  • §15, §16: test builds named and confined; socket slots reserved for administrators and sessions; agent run; new metrics (B-13, B-15, B-1).
  • §17a: 𝒯 accounted as platform trust, proof-validity trust (now including the specifications, Verus, Z3, and rustc), and the internal-unproven count; every proved invariant gets a gym entry written from the English (B-4, B-14).
  • Not spec changes: guarantees.md says Kani runs by hand, but CI runs it (B-17; correct the doc).

§28. Changes from v4.5

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Incorporates the Phase A close-out review of Oct 30 against commit 805cd1c: four read-only code audits of Oct 29 (ids C/P/D/K/H in the review document), the third-party handoff of Oct 30 (R01–R16, S01–S07), and the owner decisions of Oct 30. Ids in brackets name the finding each change closes; the review document in the project carries the file:line evidence for each.

New invariants and schema

  • I24: agent authority bound per instance; AgentStarted { name, instance, by }; agent run refuses a name in use; B-20 (High) registered and closed by this [new finding; R05 context].
  • I25: post-commit bookkeeping on every commit path; a commit whose floor record cannot be written does not commit [R02, C8].
  • I6 and §12 Concurrency carry the same exclusive-state list, now including Checking and Publishing; TxnState::exclusive is that list and the ledger reads it [S07, D11, C20].
  • I7: probe evidence per probe and the approver’s identity in the record [C1, C17]. I8: re-validation also when leaving Built [C2, P1]. I12: parked states bounded or reported [S03]. I20: launched agents inside the rule [R05]. I23: the override CAS is under the daemon’s lock; X-12 for writers outside it [R11].
  • TxnState::Publishing for the boot-only path [R01]. RejectReason::{Precondition, ActivationUnknown} [C3, R10].

§9 and §10

  • verify_candidate checks the recorded NAR hash of the store path, not existence; the dry activation must succeed, in time, untruncated [C6/P2, R10].
  • Promote/restore under flock; txn/<id>/ by exclusive mkdir and deleted after terminal + checkpoint; the content-addressed override store pruned [C5, R11].
  • Orphan build roots aged out at startup and before every GC, including crashed builds [C4/P5].
  • Stale check, re-derive and re-decide on every path into Activating, including from Built [C2].
  • Executor-owned post-check per probe against the pre-change set, and over every observed unit [R07, C22].
  • Probe timeout is a whole-probe deadline; the batch never waits past it; typed probes’ sandboxing deferred to C with bytes_max enforced for every reading kind [R06, P10]. Slow ticks warned [P9].
  • Graphical expectation for seated sessions only; SessionEstablished unchanged [S05].
  • External dependencies need a traversable edge (Requires or Diagnostic ≥ 0.7); the module rejects weaker ones [S06, P12].
  • Boot confirmation as two milestones, admission and confirmation [S02].

§11

  • Equivalent effects, equivalent facts: SetOverride{Stopped|Absent|Detected} carries the effects it causes; existing-generation operations derive from a dry activation of the target and its recorded override content [R03/C11, R04/C7].
  • Preconditions checked by the executor for every proposal [C3].
  • Agent identity per instance; Actor::Model authorized as an agent, never as the daemon [new finding, C14].

§12, §12a–§12h

  • Admission before work: authorization, catalog membership and the interlock checked before stage/evaluate/build; prebuild work bounded and charged [R09].
  • Tokens die with the daemon; consumed nonces in the checkpoint; approvals recorded with approver and bound_to; the approved execution logged [S07, C1].
  • Boot-only path: Publishing journaled before publish_boot_once; the publishing generation is the fallback, never known_good automatically; crash-table row for Publishing; AwaitingBoot never re-prepared [R01, S01].
  • Re-prepared transactions keep their original caller and source [C21].
  • Post-commit bookkeeping in the kernel’s commit on every path [R02, C8]. Floor derived relative to every earlier generation, so a Revert sets no floor [C9/H17].
  • Worker subprocess wording replaced: a worker thread in A; Lease.worker_pid/jobs reserved for D (deviations row) [C13/P3].
  • §12a write-backs queue behind an active generation transaction [P7]. §12f: parking in Built bounded at 24 h; AwaitingBoot reported as waiting; deferred repairs are deferred in fact [S03, P8]. §12c: a damaged checkpoint or record is Integrity, never Absent [C16].
  • §12d: launched agents in planarian-agents.slice, ordered and conditioned on readiness, stopped wholesale by the posture, with per-launch and per-uid limits [R05]; greetd ordered when it is the display manager [P13]; “administrators” means Planarian’s, not wheel [P14]; planarian-recovery has a start limit and raises Crashed [P15]; weakening posture/timeout takes effect at the next boot because the posture units read the booted generation [P16].

§13–§16

  • Incident control state changes only on an authorized, successful human action [R08].
  • The repair:reapply catalog entry ships [P22].
  • Residue notice teaches the attributed form [P23] (unchanged text in §14; the requirement is restated as a §15 case).
  • §15: the v4.6 acceptance cases; the harness contract (runners exit nonzero on ERROR/FAIL/PARTIAL and on unwaived UNSUPPORTED; CI strict) [R12, R15]; invisibility measured over service cgroups with a real p99 [R13].
  • §16: report boot, posture exist; ack <unit>; mark-good’s TTY from the daemon; agent run refuses names in use [P17, P18, K10].

§17a, §17b, §18, §19, §6

  • §17a: the accounting is checked (no duplicate ids, baseline invariants required, obligations tied to verifier output) [R14, D1]; the Phase A proof status stated, with the residue Kani harnesses and the I17 proof named as still required [D4].
  • §17b: three tables, Closed rows kept; B-20 and X-12 [D13].
  • §17c (new, Oct 31): the guarantees site; bypass rows name the invariants they limit; rule 22.
  • I6, §10, §12 Concurrency (owner decision, Oct 31): unit and device repairs run during a boot-only Promoted, and a canonical unit that needed a repair since admission means the change is not confirmed.
  • §12f Rollbacks (owner decision, Oct 31): a user- or intent-requested rollback runs at once; a diagnoser rollback after failed in-place repair is a repair, exempt from activation windows but not from the disruption floors, and deferred with its incident under quiet mode unless its target is a foreground dependency; the revert of a failing candidate is never deferred. Three §15 cases.
  • §18: rules 19–21 (the handoff order; equivalent effects; nothing from a failed tool); rule 4’s preserve list to I25 [D5].
  • §19: the Oct 30 decisions recorded; two owner items added [Q7, Q8].
  • §6: the layout describes the repository as it is [D14, H14].

Documentation, to be corrected in the sweep (not spec changes): ga-status.md describes the pre-805cd1c agent model and two of three verification numbers; guarantees.md carries the v4.4 trust count and P9; AGENTS.md lacks rules 12–21 and says the console is unbuilt; catalog-review.md counts a 71-entry catalog; phase-a.toml says v4.4; vm.yml and AGENTS.md count 7/62 jobs and 70 runner minutes; assumptions.md P5 names txn::journal::chain; verification.md cites §8 for §7 [D6–D10, D12, H2–H4, R16]. docs/why.md is still to be written; the review’s §5 has notes on the draft.

§Abstract

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions

Planarian is a self-healing layer for declaratively configured computers. It keeps three things distinct: what the system is meant to be (desired state, with conditions and user overrides), what it observes (with explicit unknowns and stale evidence), and whether what it observes is healthy (probes with a contract). A divergence between desired and observed state opens an incident. Incidents are repaired deterministically first, by a planner that searches typed transitions over a dependency graph; a small local language model is consulted only when deterministic repair fails or a user’s request is too fuzzy to match. Every change, whatever proposed it, passes through one executor that derives the risk classification itself, runs the change as a durable transaction with a journal that survives the daemon dying, and reverts it if health checks fail. The executor is a reference monitor: always invoked, tamperproof, and small enough to verify. Everything above it is a principal with an identity and a set of things it may request, and none holds authority on its own: not a user, not root acting through Planarian, not the on-device model, not an agent (an agent acts with exactly the permissions of the person who started it, under its own name in the record, and in Phase A can approve nothing, since approval needs a person at a terminal). Each submits typed proposals.

Planarian ships first as a Rust service on NixOS 26.05, where it is useful immediately and gathers real fault data. Its backends (service supervisor, configuration evaluator, snapshot filesystem, kernel) sit behind contracts and are replaced with Rust implementations over later phases, converging on an all-Rust operating system built on the Asterinas kernel. The contract stays the same at every step; what sits underneath it changes.


Part I — For human readers

§Sources

planarian @ a924e09 · Spec v4.6 · snapshot main · all versions