1use planarian_exec::Submitted;
4use planarian_ipc::Response;
5use planarian_schema::*;
6use planarian_txn::ledger::Record;
7use planarian_txn::machine::tag;
8
9pub fn value(v: &Value) -> String {
10 match v {
11 Value::Bool(b) => b.to_string(),
12 Value::Int(i) => i.to_string(),
13 Value::Str(s) => s.clone(),
14 Value::List(l) => format!("[{}]", l.iter().map(value).collect::<Vec<_>>().join(", ")),
15 }
16}
17
18pub fn until(u: &Until) -> String {
19 match u {
20 Until::Explicit => "until you lift it".into(),
21 Until::NextBoot => "until the next boot".into(),
22 Until::At(t) => format!("until {}", t.0),
23 }
24}
25
26pub fn transition(t: &Transition) -> String {
27 match t {
28 Transition::RestartUnit(u) => format!("restart {u}"),
29 Transition::StopUnit(u) => format!("stop {u}"),
30 Transition::BindDriver { device, driver } => format!("bind {} to {device}", driver.name),
31 Transition::ApplyChange { change, args } => {
32 let a: Vec<String> = args.iter().map(|(k, v)| format!("{k}={}", value(v))).collect();
33 format!("apply {} {}", change.0, a.join(" ")).trim_end().to_string()
34 }
35 Transition::SwitchGeneration(g) => format!("switch to generation {}", g.0),
36 Transition::Revert { to } => format!("revert to generation {}", to.0),
37 Transition::Rollback => "roll back to the known-good generation".into(),
38 Transition::LoadModel(m) => format!("load model {}", m.0),
39 Transition::RestoreSnapshot(s) => format!("restore snapshot {}", s.0),
40 Transition::CollectGarbage { emergency: true } => "emergency garbage collection".into(),
41 Transition::CollectGarbage { .. } => "garbage collection".into(),
42 Transition::SetRuntimeOption { path, value: v } => format!("set {path} to {}", value(v)),
43 Transition::SetOverride(s) => match s.desired {
44 OverrideDesired::LeaveAlone => format!("leave {} alone {}", s.unit, until(&s.until)),
45 OverrideDesired::Stopped => format!("stop {} and keep it stopped {}", s.unit, until(&s.until)),
46 OverrideDesired::Absent => format!("disable {} and keep it off {}", s.unit, until(&s.until)),
47 OverrideDesired::Detected => format!("detach {} and keep it detached {}", s.unit, until(&s.until)),
48 },
49 Transition::LiftOverride(u) => format!("stop leaving {u} alone"),
50 Transition::StopAndSuppress { unit, until: u, .. } => format!("stop {unit} and leave it stopped {}", until(u)),
51 Transition::Resolve { txn, resolution } => format!("resolve transaction {} as {resolution:?}", txn.0),
52 }
53}
54
55pub fn record(r: &Record) -> String {
56 match r {
57 Record::Prepared(t) => {
58 format!("txn {}: prepared: {} (by {:?})", t.id.0, transition(&t.transition), t.facts.caller)
59 }
60 Record::Advance { txn, to, note, .. } if note.is_empty() => format!("txn {}: {}", txn.0, tag(to)),
61 Record::Advance { txn, to, note, .. } => format!("txn {}: {} — {note}", txn.0, tag(to)),
62 Record::Takeover { txn, lease, .. } => {
63 format!("txn {}: taken over by {:?} (epoch {})", txn.0, lease.owner, lease.epoch)
64 }
65 Record::Renew { txn, .. } => format!("txn {}: lease renewed", txn.0),
66 Record::OpBegin { op, call } => format!("txn {}: {call}", op.txn.0),
67 Record::OpAck { op, outcome } => format!("txn {}: → {outcome:?}", op.txn.0),
68 Record::OverrideSet(s) => format!("{} left alone {} (by {:?})", s.unit, until(&s.until), s.by),
69 Record::OverrideLifted(u) => format!("{u} no longer left alone"),
70 Record::Incident(i) => format!(
71 "incident {} on {}: {:?}, {} attempt(s), budget {}",
72 i.id.0,
73 i.target,
74 i.state,
75 i.attempts.len(),
76 i.budget_left
77 ),
78 Record::KnownGood(g) => format!("generation {} confirmed known-good", g.0),
79 Record::LiveTested(Some(g)) => format!("generation {} passed its live tests", g.0),
80 Record::LiveTested(None) => "live-tested generation cleared".into(),
81 Record::AcceptedBaseline(g) => format!("accepted baseline: {g:?}"),
82 Record::SecurityFloor(g) => format!("security floor: {g:?}"),
83 Record::ExternalAccepted { seq, .. } => format!("external desired state {seq} accepted"),
84 Record::TokenUsed { approver, txn, .. } => match (*approver, *txn) {
85 (0, _) => "approval token used".into(),
86 (a, t) if a < 1 << 32 => format!("transaction {t} approved by uid {a}"),
87 (a, t) => format!("transaction {t} approved by principal {a}"),
88 },
89 Record::Resolved { txn, resolution, by } => format!("txn {} resolved as {resolution:?} by {by:?}", txn.0),
90 Record::Reconciliation(r) => format!("reconciliation by {:?}: {}", r.by, r.actions.join("; ")),
91 Record::AgentStarted { name, by, .. } => format!("agent {name} started by uid {by}, acting for it"),
92 Record::HoldApplied { unit, by } => format!("traffic held for {unit} ({by:?})"),
93 Record::HoldLifted { unit, reason } => format!("traffic hold on {unit} lifted: {reason}"),
94 }
95}
96
97pub fn evidence(s: &SystemState, u: &UnitId) -> String {
98 let Some((o, ev)) = s.units.get(u) else { return String::new() };
99 let mut parts = vec![format!("{o:?}")];
100 for e in ev {
101 let r = match &e.result {
102 ProbeResult::Pass => "pass".to_string(),
103 ProbeResult::Fail(m) => format!("FAIL ({m})"),
104 ProbeResult::Timeout => "timeout".to_string(),
105 ProbeResult::Abandoned => "abandoned at the batch deadline".to_string(),
106 ProbeResult::Error(m) => format!("error ({m})"),
107 };
108 let stale = if e.fresh_at(s.observed_at) { "" } else { ", stale" };
109 parts.push(format!("{}: {r}{stale}", e.probe.0));
110 }
111 parts.join("; ")
112}
113
114pub fn submitted_short(s: &Submitted) -> String {
115 match s {
116 Submitted::Rejected { reason, detail, .. } => format!("{reason:?}: {detail}"),
117 Submitted::Parked { approve, .. } => format!("needs approval {approve}"),
118 Submitted::Finished { state, .. } => tag(state).to_string(),
119 Submitted::Error(e) => e.clone(),
120 }
121}
122
123pub fn facts_summary(f: &DerivedFacts) -> String {
124 let mut why = vec![];
125 if f.sensitivity.contains(Sensitivity::Security) {
126 why.push("it touches security-relevant settings".to_string());
127 }
128 if f.sensitivity.contains(Sensitivity::External) {
129 why.push("it affects external connectivity".to_string());
130 }
131 if f.sensitivity.contains(Sensitivity::UserData) {
132 why.push("it touches user data".to_string());
133 }
134 if f.disruption.session_loss {
136 why.push("it would end your graphical session".into());
137 }
138 if f.disruption.data_interruption {
139 why.push("it would interrupt a data service".into());
140 }
141 if f.disruption.connectivity_loss {
142 why.push("it would drop network connectivity".into());
143 }
144 if f.recovery == Recovery::NonReversible {
145 why.push("it cannot be undone".into());
146 }
147 if f.touches_manifest {
148 why.push("it changes Planarian's own manifest".into());
149 }
150 if f.touches_canonical_probe {
151 why.push("it changes a canonical health check".into());
152 }
153 if why.is_empty() {
154 why.push("the autonomy setting requires approval".into());
155 }
156 let units: Vec<String> = f.affected_units.iter().map(|u| u.to_string()).collect();
157 format!("{}; affects {}", why.join(", "), if units.is_empty() { "no units".into() } else { units.join(", ") })
158}
159
160pub fn response(s: &Submitted) -> Response {
161 match s {
162 Submitted::Finished { txn, state, .. } => {
163 let message = match state {
164 TxnState::Committed => "done".to_string(),
165 TxnState::Reverted => "the change failed its health checks and was reverted; nothing changed".to_string(),
166 TxnState::RecoveryFailed => "the change failed AND reverting it failed; autonomous repair is stopped. See `planarian status`, then `planarian resolve`".to_string(),
167 TxnState::Rejected(r) => format!("not done: {r:?}"),
168 TxnState::Cancelled(_) => "withdrawn".to_string(),
169 s => tag(s).to_string(),
170 };
171 Response::Done { txn: *txn, state: state.clone(), message }
172 }
173 Submitted::Parked { txn, approve, facts } => Response::NeedsApproval {
174 txn: *txn,
175 approve: *approve,
176 what: format!("transaction {}", txn.0),
177 why: facts_summary(facts),
178 },
179 Submitted::Rejected { reason, detail, .. } => {
180 Response::Rejected { reason: format!("{reason:?}"), message: detail.clone() }
181 }
182 Submitted::Error(e) => Response::Error { message: e.clone() },
183 }
184}
185
186pub fn bytes(n: u64) -> String {
188 const GB: f64 = 1e9;
189 const MB: f64 = 1e6;
190 let x = n as f64;
191 if x >= GB { format!("{:.1} GB", x / GB) } else { format!("{:.0} MB", x / MB) }
192}
193
194pub fn when(t: planarian_schema::Timestamp) -> String {
196 let secs = t.0 / 1000;
197 let (days, rem) = (secs / 86_400, secs % 86_400);
198 let z = days as i64 + 719_468;
200 let era = z.div_euclid(146_097);
201 let doe = z - era * 146_097;
202 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
203 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
204 let mp = (5 * doy + 2) / 153;
205 let d = doy - (153 * mp + 2) / 5 + 1;
206 let m = if mp < 10 { mp + 3 } else { mp - 9 };
207 let y = yoe + era * 400 + i64::from(m <= 2);
208 format!("{y:04}-{m:02}-{d:02} {:02}:{:02}:{:02} UTC", rem / 3600, rem % 3600 / 60, rem % 60)
209}