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