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

§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.