1#![allow(clippy::result_large_err)] pub mod derive;
19pub mod host;
20
21use derive::{DeriveInput, derive, manifest_of};
22use host::ExecHost;
23use planarian_changes::CatalogConfig;
24use planarian_policy::{Input, Settings, Tables, decide, may_approve, policy_version};
25use planarian_schema::spec::{precondition, predict, refines};
26use planarian_schema::*;
27use planarian_state::Canonical;
28use planarian_txn::journal::{Journal, JournalError};
29use planarian_txn::kernel::{Kernel, KernelError};
30use planarian_txn::ledger::{LedgerError, Record};
31use planarian_txn::store::Store;
32use serde::{Deserialize, Serialize};
33use std::collections::BTreeMap;
34
35pub const GENERATION_DEADLINE: Millis = Millis::secs(15 * 60);
36pub const NETWORK_DEADLINE: Millis = Millis::secs(90);
37
38#[derive(Clone, Debug)]
39pub struct Config {
40 pub settings: Settings,
41 pub tables: Tables,
42 pub catalog: CatalogConfig,
43 pub owner: PinnedSource,
44 pub canonical: Canonical,
45}
46
47#[allow(clippy::large_enum_variant)]
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
51pub enum Submitted {
52 Rejected {
53 reason: RejectReason,
54 detail: String,
55 digest: Option<Hash>,
56 },
57 Parked {
59 txn: TxnId,
60 approve: Hash,
61 facts: DerivedFacts,
62 },
63 Finished {
64 txn: TxnId,
65 state: TxnState,
66 followup: Option<Proposal>,
67 },
68 Error(String),
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
72pub struct Parked {
73 pub txn: TxnId,
74 pub proposal: Proposal,
75 pub authz: Authorization,
76 pub class: ApproverClass,
78 pub bound_to: Hash,
79 pub issued: Timestamp,
82 pub facts: DerivedFacts,
83}
84
85#[derive(Clone)]
87struct Plan {
88 transition: Transition,
89 facts: DerivedFacts,
90 decision: Decision,
91 base_eval: EvaluatedGeneration,
92 cand_eval: Option<EvaluatedGeneration>,
93 built: Option<Built>,
94 staged: Option<StagedSource>,
95 candidate: Option<(GenerationId, StorePath)>,
96 base_override: Option<Hash>,
97 candidate_override: Option<Hash>,
98 existing_activation: Option<ActivationPlan>,
101 authz: Authorization,
104 live: OverrideSet,
105 cand_overrides: Option<OverrideSet>,
106}
107
108impl Plan {
109 fn activation(&self) -> Option<&ActivationPlan> {
110 self.built.as_ref().map(|b| &b.activation).or(self.existing_activation.as_ref())
111 }
112}
113
114pub struct Executor<B: Backends + Clone, S: Store> {
115 pub kernel: Kernel<S, ExecHost<B>>,
116 pub b: B,
117 pub cfg: Config,
118 pub session: Session,
119 pub parked: BTreeMap<TxnId, Parked>,
120 pub last: Option<(DerivedFacts, Decision)>,
123 pub excluded: Vec<String>,
126 pub prebuilt: Option<Prebuilt>,
130 pub floor_raised: Option<(GenerationId, Vec<String>)>,
133 waiting: BTreeMap<TxnId, (Timestamp, Plan)>,
136}
137
138#[derive(Clone, Debug)]
140pub struct Candidate {
141 staged: StagedSource,
142 eval: EvaluatedGeneration,
143 built: Built,
144 merged: OverrideSet,
145}
146
147#[derive(Clone, Debug)]
149pub struct Prebuilt {
150 transition: Transition,
151 live_hash: Hash,
152 result: Result<Candidate, Submitted>,
153}
154
155pub struct PrebuildJob<G> {
161 gens: G,
162 owner: PinnedSource,
163 catalog: CatalogConfig,
164 transition: Transition,
165 live: OverrideSet,
166 live_hash: Hash,
167}
168
169impl<G: GenerationBackend> PrebuildJob<G> {
170 pub fn run(self) -> Prebuilt {
171 let result = build_candidate(&self.gens, &self.owner, &self.catalog, &self.transition, &self.live);
172 Prebuilt { transition: self.transition, live_hash: self.live_hash, result }
173 }
174}
175
176fn build_candidate<G: GenerationBackend>(
177 gens: &G,
178 owner: &PinnedSource,
179 catalog: &CatalogConfig,
180 transition: &Transition,
181 live: &OverrideSet,
182) -> Result<Candidate, Submitted> {
183 let Transition::ApplyChange { change, args } = transition else {
184 return Err(reject(RejectReason::Other("prebuild".into()), "not a catalog change"));
185 };
186 let set = planarian_changes::handle(catalog, change, args).map_err(|e| match e {
187 planarian_changes::CatalogError::NotInCatalog(_) => reject(RejectReason::NotInCatalog, e.to_string()),
188 e => reject(RejectReason::Other("arguments".into()), e.to_string()),
189 })?;
190 let merged = planarian_changes::merge(live, &set);
191 let fail = |what: &str, o: Outcome| reject(RejectReason::Other(what.into()), outcome_msg(o));
192 let no_space = |what: &str, o: Outcome| {
193 if outcome_msg(o.clone()).contains("No space") {
194 reject(RejectReason::NoSpace, outcome_msg(o))
195 } else {
196 fail(what, o)
197 }
198 };
199 let staged = gens.stage(owner, &merged).map_err(|o| no_space("stage", o))?;
200 let eval = gens.evaluate(&staged).map_err(|o| fail("evaluate", o))?;
201 let built = gens.build(&eval).map_err(|o| {
202 if outcome_msg(o.clone()).contains("dry activation") {
203 reject(RejectReason::ActivationUnknown, outcome_msg(o))
204 } else {
205 no_space("build", o)
206 }
207 })?;
208 Ok(Candidate { staged, eval, built, merged })
209}
210
211pub fn unmet(t: &Transition, s: &SystemState, defs: &UnitDefs, by: &Actor) -> Option<String> {
219 if let Transition::SetOverride(sup) = t {
220 let o = s.override_of(&sup.unit)?;
221 if o.attribution == Attribution::Attested && o.by.principal() != by.principal() {
222 let holder = o.by.uid().map(|u| format!("uid {u}")).unwrap_or_else(|| format!("{:?}", o.by));
223 return Some(format!("{} holds an override set by {holder}; they lift it (planarian release)", sup.unit));
224 }
225 return None;
226 }
227 (!precondition(t, s, defs)).then(|| format!("the precondition of {t:?} does not hold"))
228}
229
230pub fn token_expired(now: Timestamp, issued: Timestamp) -> bool {
231 now > issued.plus(TOKEN_LIFETIME)
232}
233
234fn reject(reason: RejectReason, detail: impl Into<String>) -> Submitted {
235 Submitted::Rejected { reason, detail: detail.into(), digest: None }
236}
237
238fn outcome_msg(o: Outcome) -> String {
239 match o {
240 Outcome::Succeeded => "succeeded".into(),
241 Outcome::Failed(m) | Outcome::Partial(m) | Outcome::Unknown(m) => m,
242 }
243}
244
245impl<B: Backends + Clone, S: Store> Executor<B, S> {
246 pub fn new(journal: Journal<S>, b: B, me: OwnerId, cfg: Config) -> Self {
247 let host = ExecHost::new(b.clone(), cfg.canonical.clone());
248 Executor {
249 kernel: Kernel::new(journal, host, me),
250 b,
251 cfg,
252 session: Session::default(),
253 parked: BTreeMap::new(),
254 last: None,
255 excluded: vec![],
256 prebuilt: None,
257 floor_raised: None,
258 waiting: BTreeMap::new(),
259 }
260 }
261
262 pub fn policy_version(&self) -> PolicyVersion {
263 policy_version(&self.cfg.tables)
264 }
265
266 pub fn adopt_current(&self) {
269 let gens = self.b.generations();
270 let _ = gens.age_out_build_roots();
272 if let (Ok(g), Ok((set, hash))) = (gens.current(), gens.read_overrides())
273 && gens.override_for(g).ok().flatten().is_none()
274 {
275 let _ = gens.remember_override(&set);
276 let _ = gens.record_override(g, &hash);
277 }
278 }
279
280 pub fn update_context(&mut self, ctx: planarian_state::Context, protect: Vec<GenerationId>) {
282 *self.kernel.host.ctx.lock().unwrap_or_else(|e| e.into_inner()) = ctx;
283 *self.kernel.host.protect.lock().unwrap_or_else(|e| e.into_inner()) = protect;
284 }
285
286 fn eval_of(&self, g: GenerationId) -> Result<EvaluatedGeneration, String> {
287 let gens = self.b.generations();
288 let path = gens.path_of(g).map_err(outcome_msg)?;
289 gens.evaluate_built(&path).map_err(outcome_msg)
290 }
291
292 fn plan(
293 &self,
294 p: &Proposal,
295 authz: &Authorization,
296 state: &SystemState,
297 defs: &UnitDefs,
298 prebuilt: Option<Prebuilt>,
299 ) -> Result<Plan, Submitted> {
300 let gens = self.b.generations();
301 let transition = match &p.transition {
303 Transition::SetOverride(s) => Transition::SetOverride(Override { by: authz.actor.clone(), ..s.clone() }),
304 t => t.clone(),
305 };
306 let gen_op = transition.changes_generation();
310 let base_eval = match self.eval_of(state.generation) {
311 Ok(e) => e,
312 Err(_) if !gen_op => EvaluatedGeneration {
313 source: Hash::ZERO,
314 options: BTreeMap::new(),
315 units: defs.clone(),
316 user_units: vec![],
317 activation: ActivationPlan::default(),
318 locked: Default::default(),
319 core_units: BTreeMap::new(),
320 packages: BTreeMap::new(),
321 insecure: Default::default(),
322 declarations: BTreeMap::new(),
323 inputs: BTreeMap::new(),
324 services: BTreeMap::new(),
325 },
326 Err(e) => {
327 return Err(reject(
328 RejectReason::Other("evaluation".into()),
329 format!(
330 "the running generation's manifest is unavailable ({e}); rebuild once with Planarian enabled"
331 ),
332 ));
333 }
334 };
335 let (live, live_hash) = match gens.read_overrides() {
336 Ok(x) => x,
337 Err(_) if !gen_op => (OverrideSet::default(), Hash::ZERO),
338 Err(o) => return Err(reject(RejectReason::Other("overrides".into()), outcome_msg(o))),
339 };
340
341 let mut plan = Plan {
342 transition: transition.clone(),
343 facts: derive(&DeriveInput {
344 transition: &transition,
345 defs,
346 base: &base_eval,
347 candidate: None,
348 activation: None,
349 base_overrides: &live,
350 candidate_overrides: None,
351 caller: authz.actor.clone(),
352 tables: &self.cfg.tables,
353 canonical: &self.cfg.canonical,
354 state,
355 }),
356 decision: Decision::Reject(RejectReason::Other("undecided".into())),
357 base_eval: base_eval.clone(),
358 cand_eval: None,
359 built: None,
360 staged: None,
361 candidate: None,
362 base_override: None,
363 candidate_override: None,
364 existing_activation: None,
365 authz: authz.clone(),
366 live: live.clone(),
367 cand_overrides: None,
368 };
369
370 let mut cand_overrides: Option<OverrideSet> = None;
371 match &transition {
372 Transition::ApplyChange { change, args } => {
373 let _ = (change, args);
374 let pre = match prebuilt {
377 Some(p) if p.transition == transition && p.live_hash == live_hash => Some(p),
378 Some(stale) => {
379 if let Ok(c) = &stale.result {
380 gens.release_root(&c.built.root);
381 }
382 None
383 }
384 None => None,
385 };
386 let Candidate { staged, eval, built, merged } = match pre {
387 Some(p) => p.result?,
388 None => build_candidate(gens, &self.cfg.owner, &self.cfg.catalog, &transition, &live)?,
389 };
390 let fail = |what: &str, o: Outcome| reject(RejectReason::Other(what.into()), outcome_msg(o));
391 gens.remember_override(&live).map_err(|o| fail("override", o))?;
392 let cand_hash = gens.remember_override(&merged).map_err(|o| fail("override", o))?;
393 plan.candidate = Some((built.generation, built.path.clone()));
394 plan.base_override = Some(live_hash);
395 plan.candidate_override = Some(cand_hash);
396 plan.cand_eval = Some(eval);
397 plan.built = Some(built);
398 plan.staged = Some(staged);
399 cand_overrides = Some(merged);
400 }
401 Transition::SwitchGeneration(g) | Transition::Revert { to: g } => {
402 cand_overrides = self.plan_existing(&mut plan, *g, live_hash)?;
403 }
404 Transition::Rollback => {
405 cand_overrides = self.plan_existing(&mut plan, state.known_good, live_hash)?;
406 }
407 _ => {}
408 }
409
410 plan.cand_overrides = cand_overrides;
411 plan.facts = self.facts_of(&plan, state, defs);
412
413 let predicted = predict(&transition, state, defs);
415 if let Err(e) = refines(&p.expected, &predicted, Some(plan.facts.affected_units.as_slice())) {
416 self.release(&plan);
417 return Err(reject(RejectReason::Refinement, e));
418 }
419
420 plan.decision = self.decision_of(&plan, state);
421 Ok(plan)
422 }
423
424 fn facts_of(&self, plan: &Plan, state: &SystemState, defs: &UnitDefs) -> DerivedFacts {
426 let transition = &plan.transition;
427 let mut facts = derive(&DeriveInput {
428 transition,
429 defs,
430 base: &plan.base_eval,
431 candidate: plan.cand_eval.as_ref(),
432 activation: plan.activation(),
433 base_overrides: &plan.live,
434 candidate_overrides: plan.cand_overrides.as_ref(),
435 caller: plan.authz.actor.clone(),
436 tables: &self.cfg.tables,
437 canonical: &self.cfg.canonical,
438 state,
439 });
440 let sessions = self.b.sessions().sessions().unwrap_or_default();
442 derive::session_facts(&mut facts, &sessions);
443 derive::floor_fact(&mut facts, transition, state);
444 let ended = match transition {
446 Transition::RestartUnit(u) => self.b.units().ended_by(u),
447 _ => None,
448 };
449 derive::stop_fact(&mut facts, transition, state, defs, ended);
450 facts
451 }
452
453 fn decision_of(&self, plan: &Plan, state: &SystemState) -> Decision {
454 let locked: Vec<String> =
455 plan.cand_eval.as_ref().map(|e| e.locked.iter().cloned().collect()).unwrap_or_default();
456 decide(&Input {
457 transition: &plan.transition,
458 facts: &plan.facts,
459 known_good: state.known_good,
460 live_tested: state.live_tested,
461 cumulative: &self.session.cumulative,
462 authz: &plan.authz,
463 settings: &self.cfg.settings,
464 tables: &self.cfg.tables,
465 locked: &locked,
466 revert_to: None,
467 })
468 }
469
470 fn revalidate(&mut self, id: TxnId, plan: &Plan, state: &SystemState, defs: &UnitDefs) -> Option<Submitted> {
476 let overrides_moved = plan
477 .base_override
478 .is_some_and(|b| self.b.generations().read_overrides().map(|(_, h)| h != b).unwrap_or(true));
479 let mut again = plan.clone();
480 again.facts = self.facts_of(&again, state, defs);
481 let redecided = self.decision_of(&again, state);
482 if !self.stale(plan) && !overrides_moved && redecided == plan.decision {
483 return None;
484 }
485 self.release(plan);
486 Some(match self.kernel.reject(id, RejectReason::Stale) {
487 Ok(end) => Submitted::Finished { txn: id, state: end, followup: None },
488 Err(e) => Self::kernel_error(e),
489 })
490 }
491
492 fn plan_existing(
497 &self,
498 plan: &mut Plan,
499 g: GenerationId,
500 live_hash: Hash,
501 ) -> Result<Option<OverrideSet>, Submitted> {
502 let gens = self.b.generations();
503 let fail = |o: Outcome| reject(RejectReason::Other("generation".into()), outcome_msg(o));
504 let path = gens.path_of(g).map_err(fail)?;
505 let eval = gens.evaluate_built(&path).map_err(fail)?;
506 let activation =
507 gens.dry_activation(&path).map_err(|o| reject(RejectReason::ActivationUnknown, outcome_msg(o)))?;
508 let mut content = None;
509 if let Some(h) = gens.override_for(g).map_err(fail)? {
510 content = Some(gens.override_content(&h).map_err(|o| reject(RejectReason::Precondition, outcome_msg(o)))?);
512 plan.base_override = Some(live_hash);
513 plan.candidate_override = Some(h);
514 }
515 plan.candidate = Some((g, path));
516 plan.cand_eval = Some(eval);
517 plan.existing_activation = Some(activation);
518 Ok(content)
519 }
520
521 fn release(&self, plan: &Plan) {
522 if let Some(b) = &plan.built {
523 self.b.generations().release_root(&b.root);
524 }
525 }
526
527 fn binding(&self, txn: TxnId, p: &Proposal, plan: &Plan, state: &SystemState) -> Hash {
532 let cumulative = plan.facts.config_diff.as_ref().map(|d| self.session.cumulative.then(d)).unwrap_or_default();
533 let v = serde_json::json!({
534 "txn": txn.0,
535 "transition": plan.transition,
536 "staged_source": plan.staged.as_ref().map(|s| s.staged_source),
537 "candidate": plan.candidate,
538 "base": state.generation,
539 "policy": self.policy_version(),
540 "cumulative": cumulative.hash(),
541 "facts": plan.facts,
542 "expected": p.expected,
543 "revert_to": state.generation,
544 "recovery": plan.transition.recovery(),
545 });
546 Hash::of(&serde_json::to_vec(&v).expect("binding serializes"))
547 }
548
549 fn transaction(&self, p: &Proposal, plan: &Plan, state: &SystemState, defs: &UnitDefs) -> Transaction {
550 let now = self.b.clock().now();
551 let boot = self.b.clock().boot_now();
553 let network = plan.facts.disruption.connectivity_loss
554 || plan.facts.affected_units.iter().any(|u| u.name == "NetworkManager");
555 let prior_driver = match &plan.transition {
556 Transition::BindDriver { device, .. } => self.b.inventory().bound_driver(device).ok().flatten(),
557 _ => None,
558 };
559 Transaction {
560 id: TxnId(0),
561 state: TxnState::Prepared,
562 lease: Lease {
563 txn: TxnId(0),
564 owner: self.kernel.me.clone(),
565 epoch: 1,
566 worker_pid: None,
567 renewed: boot,
568 jobs: vec![],
569 },
570 base_source: plan.staged.as_ref().map(|s| s.base_source),
571 staged_source: plan.staged.as_ref().map(|s| s.staged_source),
572 candidate_nar: plan.built.as_ref().and_then(|b| b.nar),
573 base_override: plan.base_override,
574 candidate_override: plan.candidate_override,
575 gc_root: plan.built.as_ref().map(|b| b.root.clone()),
576 recovery: plan.transition.recovery(),
577 transition: plan.transition.clone(),
578 facts: plan.facts.clone(),
579 base: state.generation,
580 candidate: plan.candidate.clone(),
581 policy: self.policy_version(),
582 approval: None,
583 snapshot: None,
584 expected: p.expected.clone(),
585 started: now,
586 deadline: boot.plus(if network { NETWORK_DEADLINE } else { GENERATION_DEADLINE }),
587 revert_to: state.generation,
588 prior_driver,
589 prepared_again_from: None,
590 check: CheckContext {
591 candidate: plan.cand_eval.as_ref().map(manifest_of),
592 base: manifest_of(&plan.base_eval),
593 pre: defs.keys().map(|u| (u.clone(), state.status(u, defs))).collect(),
594 pre_passing: defs
595 .keys()
596 .map(|u| (u.clone(), state.passing_readiness(u, defs)))
597 .filter(|(_, p)| !p.is_empty())
598 .collect(),
599 },
600 }
601 }
602
603 fn stale(&self, plan: &Plan) -> bool {
605 match &plan.staged {
606 Some(s) => self.b.generations().source_hash(&self.cfg.owner).map(|h| h != s.base_source).unwrap_or(true),
607 None => false,
608 }
609 }
610
611 fn kernel_error(e: KernelError) -> Submitted {
612 match e {
613 KernelError::Journal(JournalError::Ledger(LedgerError::Exclusive { blocker, .. })) => {
614 reject(RejectReason::Other("busy".into()), format!("transaction {} is in progress", blocker.0))
615 }
616 KernelError::Journal(JournalError::Ledger(LedgerError::Interlocked { .. })) => reject(
617 RejectReason::Other("interlocked".into()),
618 "the recovery interlock is set; an administrator must run `planarian resolve`",
619 ),
620 e => Submitted::Error(format!("{e:?}")),
621 }
622 }
623
624 pub fn prebuild_job(&self, t: &Transition) -> Option<PrebuildJob<B::Gens>>
628 where
629 B::Gens: Clone,
630 {
631 if !matches!(t, Transition::ApplyChange { .. }) {
632 return None;
633 }
634 let (live, live_hash) = self.b.generations().read_overrides().ok()?;
635 Some(PrebuildJob {
636 gens: self.b.generations().clone(),
637 owner: self.cfg.owner.clone(),
638 catalog: self.cfg.catalog.clone(),
639 transition: t.clone(),
640 live,
641 live_hash,
642 })
643 }
644
645 pub fn admit(&self, t: &Transition, authz: &Authorization) -> Result<(), (RejectReason, &'static str)> {
650 let Transition::ApplyChange { change, .. } = t else { return Ok(()) };
651 if !matches!(authz.actor, Actor::User { .. } | Actor::Daemon | Actor::Watchdog) {
652 return Err((RejectReason::Unauthorized, "this caller cannot change the configuration"));
653 }
654 if !self.cfg.tables.catalog.contains(change) {
655 return Err((RejectReason::NotInCatalog, "that change is not in the catalog"));
656 }
657 let admin = authz.capabilities.contains(Capability::Administrator);
658 if self.kernel.journal.ledger().interlocked() && !admin {
659 return Err((
660 RejectReason::Other("interlocked".into()),
661 "the recovery interlock is set; only an administrator can change this machine until `planarian resolve`",
662 ));
663 }
664 if matches!(authz.actor, Actor::User { .. })
665 && !admin
666 && !(authz.capabilities.contains(Capability::UserControllable)
667 && self.cfg.settings.user_controllable_changes.contains(change))
668 {
669 return Err((RejectReason::Unauthorized, "that change needs an administrator"));
670 }
671 Ok(())
672 }
673
674 pub fn submit(&mut self, p: Proposal, authz: Authorization, state: &SystemState, defs: &UnitDefs) -> Submitted {
676 self.submit_from(p, authz, state, defs, None)
677 }
678
679 pub fn submit_again(
685 &mut self,
686 p: Proposal,
687 from: TxnId,
688 authz: Authorization,
689 state: &SystemState,
690 defs: &UnitDefs,
691 ) -> Submitted {
692 self.submit_from(p, authz, state, defs, Some(from))
693 }
694
695 fn submit_from(
696 &mut self,
697 p: Proposal,
698 authz: Authorization,
699 state: &SystemState,
700 defs: &UnitDefs,
701 again: Option<TxnId>,
702 ) -> Submitted {
703 let digest = state.digest();
704 if p.observed != digest {
705 return Submitted::Rejected {
706 reason: RejectReason::Stale,
707 detail: "state changed since the proposal was made".into(),
708 digest: Some(digest),
709 };
710 }
711 self.last = None;
712 if self.kernel.journal.ledger().interlocked()
716 && !authz.capabilities.contains(Capability::Administrator)
717 && !matches!(p.transition, Transition::SetOverride(_) | Transition::LiftOverride(_))
718 {
719 self.prebuilt = None;
720 return reject(
721 RejectReason::Other("interlocked".into()),
722 "the recovery interlock is set; only an administrator can change this machine until `planarian resolve`",
723 );
724 }
725 if let Err((reason, why)) = self.admit(&p.transition, &authz) {
726 self.prebuilt = None;
727 return reject(reason, why);
728 }
729 let pre = self.prebuilt.take();
730 let plan = match self.plan(&p, &authz, state, defs, pre) {
731 Ok(plan) => plan,
732 Err(s) => return s,
733 };
734 self.last = Some((plan.facts.clone(), plan.decision.clone()));
735 self.excluded = plan.staged.as_ref().map(|s| s.excluded.clone()).unwrap_or_default();
736 if plan.staged.is_some() {
737 for f in self.b.generations().untracked(&self.cfg.owner) {
739 if !self.excluded.contains(&f) {
740 self.excluded.push(f);
741 }
742 }
743 }
744 if let Decision::Reject(r) = &plan.decision {
745 self.release(&plan);
746 return reject(r.clone(), "refused by policy");
747 }
748 if let Some(why) = unmet(&p.transition, state, defs, &authz.actor) {
751 self.release(&plan);
752 return reject(RejectReason::Precondition, why);
753 }
754 if self.stale(&plan) {
755 self.release(&plan);
756 return reject(RejectReason::Stale, "your configuration changed while the change was being built");
757 }
758 let mut txn = self.transaction(&p, &plan, state, defs);
759 txn.prepared_again_from = again;
760 let id = match self.kernel.prepare(txn) {
761 Ok(id) => id,
762 Err(e) => {
763 self.release(&plan);
764 return Self::kernel_error(e);
765 }
766 };
767 if let Decision::Allow { tier: Tier::AskFirst, approver: class } = plan.decision {
768 let bound_to = self.binding(id, &p, &plan, state);
769 let now = self.b.clock().boot_now();
770 self.parked.insert(
771 id,
772 Parked { txn: id, proposal: p, authz, class, bound_to, issued: now, facts: plan.facts.clone() },
773 );
774 return Submitted::Parked { txn: id, approve: bound_to, facts: plan.facts };
775 }
776 self.schedule(id, plan, &p.source, state)
777 }
778
779 fn schedule(&mut self, id: TxnId, plan: Plan, source: &Source, state: &SystemState) -> Submitted {
795 if !plan.transition.changes_generation() {
796 return self.run(id, &plan);
797 }
798 if plan.built.as_ref().is_some_and(|b| b.activation.boot_only()) {
799 return match self.kernel.run_boot_only(id) {
800 Ok(end) => Submitted::Finished { txn: id, state: end, followup: None },
801 Err(e) => Self::kernel_error(e),
802 };
803 }
804 let foreground = state.conditions.iter().any(|c| matches!(c, Condition::Foreground { .. }));
805 let user_asked = matches!(source, Source::User | Source::Intent { .. });
806 let rollback = match plan.transition {
807 Transition::Rollback => user_asked || matches!(source, Source::Diagnoser { .. }),
808 Transition::Revert { .. } => user_asked,
809 _ => false,
810 };
811 if rollback {
812 let waiting: Vec<TxnId> = self.waiting.keys().copied().collect();
813 for w in waiting {
814 if self.kernel.withdraw(w, id).is_ok() {
815 self.waiting.remove(&w);
816 }
817 }
818 return self.run(id, &plan);
819 }
820 let disturbs = plan.built.as_ref().is_some_and(|b| self.disturbs_a_session(&b.activation));
821 if foreground || (disturbs && !user_asked && !self.sessions_idle()) {
822 let why = if foreground { "waiting: foreground work holds" } else { "waiting: a session is in use" };
823 return match self.kernel.wait_built(id, why) {
824 Ok(end) => {
825 self.waiting.insert(id, (self.b.clock().boot_now(), plan));
826 Submitted::Finished { txn: id, state: end, followup: None }
827 }
828 Err(e) => Self::kernel_error(e),
829 };
830 }
831 self.run(id, &plan)
832 }
833
834 fn disturbs_a_session(&self, a: &ActivationPlan) -> bool {
837 let sessions = self.b.sessions().sessions().unwrap_or_default();
838 if !sessions.iter().any(|s| s.establishes() && s.active) {
839 return false;
840 }
841 a.stop.iter().chain(&a.restart).chain(&a.reload).any(|u| {
842 matches!(u.scope, Scope::User { .. }) || ["display-manager", "graphical-session"].contains(&u.name.as_str())
843 })
844 }
845
846 fn sessions_idle(&self) -> bool {
847 self.b.sessions().sessions().unwrap_or_default().iter().filter(|s| s.establishes() && s.active).all(|s| s.idle)
848 }
849
850 pub fn activate_waiting(&mut self, state: &SystemState, defs: &UnitDefs) -> Vec<(TxnId, Submitted)> {
854 let foreground = state.conditions.iter().any(|c| matches!(c, Condition::Foreground { .. }));
855 if foreground || self.waiting.is_empty() {
856 return vec![];
857 }
858 let ready: Vec<TxnId> = self
859 .waiting
860 .iter()
861 .filter(|(_, (_, p))| {
862 !p.built.as_ref().is_some_and(|b| self.disturbs_a_session(&b.activation)) || self.sessions_idle()
863 })
864 .map(|(id, _)| *id)
865 .collect();
866 let mut out = vec![];
867 for id in ready {
868 if let Some((_, plan)) = self.waiting.remove(&id) {
869 if let Some(refused) = self.revalidate(id, &plan, state, defs) {
872 out.push((id, refused));
873 continue;
874 }
875 out.push((id, self.run(id, &plan)));
876 }
877 }
878 out
879 }
880
881 fn run(&mut self, id: TxnId, plan: &Plan) -> Submitted {
882 match self.kernel.run(id) {
883 Ok(end) => {
884 let followup = self.after(id, plan, &end);
885 Submitted::Finished { txn: id, state: end, followup }
886 }
887 Err(e) => Self::kernel_error(e),
888 }
889 }
890
891 fn after(&mut self, id: TxnId, plan: &Plan, end: &TxnState) -> Option<Proposal> {
893 if *end != TxnState::Committed {
894 return None;
895 }
896 if let (Some((g, _)), Some(cand)) = (&plan.candidate, &plan.cand_eval)
899 && self.kernel.journal.ledger().security_floor == Some(*g)
900 {
901 self.floor_raised = Some((*g, cand.security_fixes(&plan.base_eval)));
902 }
903 self.session.applied.push(id);
904 if let Some(d) = &plan.facts.config_diff {
905 self.session.cumulative = self.session.cumulative.then(d);
906 }
907 if let Transition::SetRuntimeOption { path, value } = &plan.transition {
908 let (change, args) = planarian_changes::writeback(path, value)?;
909 return Some(Proposal {
910 observed: Hash::ZERO, transition: Transition::ApplyChange { change, args },
912 expected: Expectation {
913 units: BTreeMap::new(),
914 generation: None,
915 may_change: Default::default(),
916 others_unchanged: false,
917 settle: Settle::Duration(planarian_schema::spec::DEFAULT_SETTLE),
918 },
919 reason: format!("persist the runtime setting {path}"),
920 source: Source::User,
921 });
922 }
923 None
924 }
925
926 pub fn approve(
929 &mut self,
930 hash: &Hash,
931 approver: &Authorization,
932 state: &SystemState,
933 defs: &UnitDefs,
934 ) -> Submitted {
935 let Some(parked) = self.parked.values().find(|p| p.bound_to == *hash).cloned() else {
936 return reject(RejectReason::Other("approval".into()), "no pending change with that hash");
937 };
938 if !may_approve(approver, parked.authz.principal, parked.class) {
941 let why = if approver.principal == parked.authz.principal {
942 "you can't approve your own request"
943 } else if parked.class == ApproverClass::Administrator {
944 "this needs an administrator's approval"
945 } else {
946 "you can't approve requests"
947 };
948 return reject(RejectReason::Unauthorized, why);
949 }
950 let now = self.b.clock().boot_now();
951 if token_expired(now, parked.issued) {
952 return self.expire_one(parked.txn);
953 }
954 let staged_base = self
957 .kernel
958 .journal
959 .sync_ledger()
960 .ok()
961 .and_then(|l| l.live.get(&parked.txn).and_then(|t| t.txn.base_source));
962 if let Some(base) = staged_base
963 && self.b.generations().source_hash(&self.cfg.owner).map(|h| h != base).unwrap_or(true)
964 {
965 self.parked.remove(&parked.txn);
966 let _ = self.kernel.reject(parked.txn, RejectReason::Stale);
967 return reject(RejectReason::Stale, "your configuration changed after the approval was requested");
968 }
969 let pre = self.prebuilt.take();
970 let plan = match self.plan(&parked.proposal, &parked.authz, state, defs, pre) {
971 Ok(p) => p,
972 Err(s) => {
973 self.parked.remove(&parked.txn);
974 let _ = self.kernel.reject(parked.txn, RejectReason::Other("approval invalidated".into()));
975 return s;
976 }
977 };
978 let rebound = self.binding(parked.txn, &parked.proposal, &plan, state);
979 if rebound != parked.bound_to || !matches!(plan.decision, Decision::Allow { .. }) {
980 self.parked.remove(&parked.txn);
983 let _ = self.kernel.reject(parked.txn, RejectReason::Other("approval invalidated".into()));
984 self.release(&plan);
985 let mut p = parked.proposal.clone();
986 p.observed = state.digest();
987 return self.submit(p, parked.authz, state, defs);
988 }
989 if self.stale(&plan) {
990 self.parked.remove(&parked.txn);
991 let _ = self.kernel.reject(parked.txn, RejectReason::Stale);
992 return reject(RejectReason::Stale, "your configuration changed after the approval was requested");
993 }
994 let mut nonce = [0u8; 16];
996 nonce.copy_from_slice(&hash.0[..16]);
997 if self.kernel.journal.ledger().tokens_used.iter().any(|(n, _)| *n == nonce) {
998 return reject(RejectReason::Other("approval".into()), "this approval was already used");
999 }
1000 let _ = self.kernel.journal.append(
1003 self.b.clock().now(),
1004 Record::TokenUsed {
1005 nonce,
1006 expires: parked.issued.plus(TOKEN_LIFETIME),
1007 approver: approver.principal.0,
1008 txn: parked.txn.0,
1009 },
1010 );
1011 self.parked.remove(&parked.txn);
1012 let source = parked.proposal.source.clone();
1013 self.schedule(parked.txn, plan, &source, state)
1014 }
1015
1016 fn expire_one(&mut self, txn: TxnId) -> Submitted {
1017 self.parked.remove(&txn);
1018 match self.kernel.reject(txn, RejectReason::ApprovalExpired) {
1019 Ok(s) => Submitted::Finished { txn, state: s, followup: None },
1020 Err(e) => Self::kernel_error(e),
1021 }
1022 }
1023
1024 pub fn expire(&mut self) -> Vec<TxnId> {
1026 let now = self.b.clock().boot_now();
1027 let old: Vec<TxnId> = self.parked.values().filter(|p| token_expired(now, p.issued)).map(|p| p.txn).collect();
1028 for t in &old {
1029 self.expire_one(*t);
1030 }
1031 old
1032 }
1033
1034 pub fn is_waiting(&self, txn: TxnId) -> bool {
1036 self.waiting.contains_key(&txn)
1037 }
1038
1039 pub fn waiting_since(&self, txn: TxnId) -> Option<Timestamp> {
1041 self.waiting.get(&txn).map(|(since, _)| *since)
1042 }
1043
1044 pub fn keep_waiting_since(&mut self, txn: TxnId, since: Timestamp) {
1047 if let Some((s, _)) = self.waiting.get_mut(&txn) {
1048 *s = (*s).min(since);
1049 }
1050 }
1051
1052 pub fn expire_waiting(&mut self) -> Vec<(TxnId, Transition)> {
1056 let now = self.b.clock().boot_now();
1057 let old: Vec<TxnId> =
1058 self.waiting.iter().filter(|(_, (since, _))| now > since.plus(PARKING_LIMIT)).map(|(id, _)| *id).collect();
1059 let mut out = vec![];
1060 for id in old {
1061 if let Some((_, plan)) = self.waiting.remove(&id)
1062 && self.kernel.cancel(id, Actor::Daemon).is_ok()
1063 {
1064 out.push((id, plan.transition));
1065 }
1066 }
1067 out
1068 }
1069
1070 pub fn cancel(&mut self, txn: TxnId, by: Actor) -> Submitted {
1072 if self.parked.remove(&txn).is_none() {
1073 return reject(RejectReason::Other("cancel".into()), "no such pending change");
1074 }
1075 match self.kernel.cancel(txn, by) {
1076 Ok(s) => Submitted::Finished { txn, state: s, followup: None },
1077 Err(e) => Self::kernel_error(e),
1078 }
1079 }
1080}
1081
1082pub mod residue;
1083
1084#[cfg(kani)]
1085mod kani;
1086#[cfg(test)]
1087mod tests;