crates/holger-monetize-guest/src/guardian.rs
//! **holger's estate guardian, in holger's own guest** (lane W3, 2026-10-19) — the holger impl
//! of [`monetize_product_guest::guardian::EstateGuardian`], gunnar's lifecycle in holger's twin
//! model. The ONE box links no product, so this is the guardian it runs for a Holger estate:
//! the guest answers `guardian.diagnose`, `guardian.plan` and `guardian.observe` with it.
//! It is the same code as monetize's linked plugin (`monetize/products/holger/src/guardian.rs`);
//! monetize's `the_built_holger_guest_guards_as_the_linked_plugin_does` holds the two equal.
//!
//! Pure: it reads what the boxes REPORTED (`Fleet.Report` health words) and answers findings
//! and plans. Nothing here dials a box. Every verb on the estate is one of holger's own
//! (`holger.v1.AdminService`), run by the box that pulled the signed order:
//!
//! | order | body | run by | holger's verbs |
//! |---|---|---|---|
//! | `become_twin` | `{primary}` | a freshly laid box | `BecomeTwin` (the election, exactly once, a FRESH box only) + `TwinCsr` |
//! | `adopt_twin` | `{twin, principal}` | the main | `IssueTwinCertificate` for `holger-twin`; the twin installs it (`InstallTwinCertificate`) |
//! | `fence_primary` | `{primary, epoch}` | the main, on itself | every door refuses writes from then on |
//! | `recharge_from_twin` | `{main, twin, accept_lost, epoch}` | a FRESH main | `TwinCsr`/`IssueTwinCertificate`/`InstallTwinCertificate` purpose `recharge`, then `Recharge` (it resumes a `restoring` one) |
//! | `review_twin` | `{}` | the main | `TwinStatus` read again, its verdict on the next report |
//! | `restate_entitlements` | `{snapshot}` (signed `Snapshot`) | the main, once recharged | `EntitlementService.Set` per fact |
//!
//! **Where holger differs from gunnar, and why the plans do.** A holger box installs from ONE
//! medium with no role and is TOLD it is the twin (`BecomeTwin`), so a new twin needs
//! `become_twin` before the main adopts it. `Recharge` runs only on a FRESH main
//! (`RechargeStatus.fresh`; a box holding state of its own is re-imaged first), so a main that
//! diverged is not recharged in place: it is replaced (New server). And a recharged main mints
//! its own issuer, so the twin is RE-POINTED at it — by the recharge itself (holger's recharge lane), so the rebuild plan is Gunnar's step for step.
//!
//! Health words a holger box reports (flat, the box's Fleet client fills them from
//! `RechargeStatus`, `TwinStatus` and the admin view's twin rows): `serving`
//! (`yes`/`no`/`fenced`), `data` (`ok` = holding or recharged, `fresh`, `restoring`),
//! `recharge` (`failed` when the last `Recharge` named failures), `serves_at` (`host:port`),
//! `twin` (`paired`/`silent`/`none`, on the main), `primary` (`live`/`silent`, on the twin),
//! `complete` (`yes`/`no`, on the twin: its last follow cycle that reached the primary was
//! whole), `epoch`.
use monetize_product_guest::guardian::rebuild::{self, Twin};
use monetize_product_guest::guardian::{
role, CloudStep, EstateGuardian, EstateView, Finding, Remedy, Step, StepState,
};
/// The one holger guardian (stateless).
pub static GUARDIAN: HolgerGuardian = HolgerGuardian;
pub struct HolgerGuardian;
use monetize_product_guest::guardian::steps::{self, heard, order, recharge, restate, wait, word};
/// The plan's building blocks and bounds are SHARED since P4a (monetize-product
/// `guardian::steps`, the same in Gunnar's guest): `order`, `wait`, `recharge`, `restate`,
/// `heard`, `word`, and how long a laid box, a pairing and a recharge may take.
pub use monetize_product_guest::guardian::steps::{
HEARD_WITHIN_MS, PAIRED_WITHIN_MS, RECHARGED_WITHIN_MS,
};
/// The twin can give the estate back whole: its last cycle that reached the primary was
/// complete. An older box that reports no `complete` word is taken at its follow's word.
fn twin_whole(v: &EstateView) -> bool {
word(v, role::TWIN, "complete") != "no"
}
/// **Holger's twin as the guard reads it** (lane G11): it still hears its primary unless it
/// says `primary=silent`; it can give the estate back when its last follow that reached the
/// primary was whole ([`twin_whole`]). Holger keeps no log head, so the guard's R4 has nothing
/// to compare (`holds: None`); the whole follow is Holger's answer to "is the main ahead".
/// `None` while the twin is silent.
fn twin_of(v: &EstateView) -> Option<Twin> {
let b = v.get(role::TWIN).filter(|_| !v.silent(role::TWIN))?;
let why = b.word("why");
Some(Twin {
addr: rebuild::twin_addr(v),
hears_primary: b.word("primary") != "silent",
recharge_possible: twin_whole(v),
verdict: if twin_whole(v) {
"its last follow was whole".into()
} else {
format!(
"its last follow was not whole ({})",
if why.is_empty() {
"no reason given"
} else {
why
}
)
},
holds: None,
})
}
impl EstateGuardian for HolgerGuardian {
fn diagnose(&self, v: &EstateView) -> Vec<Finding> {
let main_silent = v.silent(role::MAIN);
let has_twin = v.get(role::TWIN).is_some();
let twin_silent = v.silent(role::TWIN);
let twin_up = has_twin && !twin_silent;
let evidence = vec![heard(v, role::MAIN), heard(v, role::TWIN)];
if main_silent && has_twin && twin_silent {
return vec![steps::both_lost(evidence)];
}
let mut out = Vec::new();
// ★ Gunnar's rule (W3 parity, gunnar cf02854a): a re-laid main that reports itself HELD
// (`data=fresh serving=no`, M3's boot hold) is no finding of its own. It is the box the
// rebuild plan laid (New server: fence, lay, heard, recharge, recharged, restate), and
// that plan's recharge step fills it; a person may still press Recharge from twin.
if main_silent && twin_up {
let twin_blind = word(v, role::TWIN, "primary") == "silent";
let whole = twin_whole(v);
let words = match (twin_blind, whole) {
(true, true) => "the main is silent, and the twin cannot see it either: the main is rebuilt from the twin (never promoted)".to_string(),
(true, false) => format!(
"the main is silent, and the twin cannot see it either — but the twin's last follow was not whole ({}): a person decides (accept the loss, or bring the main's disk back)",
if word(v, role::TWIN, "why").is_empty() { "no reason given" } else { word(v, role::TWIN, "why") }
),
(false, _) => "the main is silent to its provider; the twin still sees it".to_string(),
};
// ★ The guard (Gunnar's `rebuild::judge`, R1-R6, shared: lane G11): silence alone
// never lays a main again. Auto-fix runs New server only where the guard says
// `lay_again` — the main GONE at the provider, a whole twin that does not hear it.
let guard = rebuild::auto_new_server(v, twin_of(v));
out.push(steps::main_lost(&words, &guard, evidence.clone()));
}
out.extend(steps::twin_lost(v, evidence));
out
}
fn plan(&self, remedy: Remedy, v: &EstateView) -> Result<Vec<Step>, String> {
let main_up = !v.silent(role::MAIN);
let twin_up = !v.silent(role::TWIN);
match remedy {
Remedy::NewTwin => {
if !main_up {
return Err("no primary is reporting to adopt a new twin: the main comes first (New server)".into());
}
Ok(vec![
Step::Cloud(CloudStep::Fence {
role: role::TWIN.into(),
}),
Step::Cloud(CloudStep::Lay {
role: role::TWIN.into(),
}),
wait(role::TWIN, "heard", HEARD_WITHIN_MS),
// A holger box is TOLD it is the twin, once, while it is fresh.
order(role::TWIN, "become_twin", &[("primary", "$addr:main")]),
order(
role::MAIN,
"adopt_twin",
&[("twin", "$addr:twin"), ("principal", "$principal")],
),
wait(role::MAIN, "paired", PAIRED_WITHIN_MS),
])
}
// The guard before the fence gives the old main back whole (lane G11).
Remedy::NewServer => steps::new_server(v, twin_of(v)),
Remedy::RechargeFromTwin => {
if !twin_up {
return Err("no twin is reporting: nothing to recharge from".into());
}
if !main_up {
return Err(
"the main is silent and cannot pull its order: press New server".into(),
);
}
let data = word(v, role::MAIN, "data");
if !matches!(data, "fresh" | "restoring") {
return Err(format!(
"the main reports data={}: holger recharges only a FRESH main (a box that holds state of its own is re-imaged, never overwritten) — press New server",
if data.is_empty() { "?" } else { data }
));
}
let mut steps = Vec::new();
if word(v, role::MAIN, "serving") == "yes" {
steps.push(order(
role::MAIN,
"fence_primary",
&[("primary", "$addr:main"), ("epoch", "$epoch")],
));
}
steps.push(recharge());
steps.push(wait(role::MAIN, "recharged", RECHARGED_WITHIN_MS));
steps.push(restate());
Ok(steps)
}
Remedy::ReviewTwin => steps::review_twin(v),
// S8: the front reports nothing on Fleet; main and twin do (shared, P4).
Remedy::RestoreFront => steps::restore_front(v),
}
}
fn observe(&self, r: &str, what: &str, v: &EstateView) -> StepState {
let Some(b) = v.get(r) else {
return StepState::Waiting(format!("the {r} is not registered yet"));
};
match what {
"paired" => steps::observe_paired(r, b),
"recharged" => steps::observe_recharged(r, b),
other => StepState::Failed(format!(
"holger's guardian does not know how to await {other:?}"
)),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use monetize_product_guest::guardian::BoxView;
use super::*;
const NOW: u64 = 1_800_000_000_000;
fn boxed(r: &str, heard_ago_ms: Option<u64>, words: &[(&str, &str)]) -> (String, BoxView) {
let health: BTreeMap<String, String> = words
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
(
r.to_string(),
BoxView {
role: r.into(),
heard_unix_ms: heard_ago_ms.map(|a| NOW - a).unwrap_or(0),
health,
..Default::default()
},
)
}
/// The provider's word on the box (the runner fills it for a silent main monetize laid).
fn at(mut b: (String, BoxView), word: &str) -> (String, BoxView) {
b.1.at_provider = word.into();
b
}
fn view(boxes: Vec<(String, BoxView)>) -> EstateView {
EstateView {
product: "holger".into(),
instance: "acme".into(),
now_unix_ms: NOW,
silent_after_ms: 90_000,
boxes: boxes.into_iter().collect(),
}
}
fn healthy_main() -> (String, BoxView) {
boxed(
"main",
Some(1_000),
&[
("serving", "yes"),
("data", "ok"),
("serves_at", "10.6.0.10:50051"),
("twin", "paired"),
],
)
}
fn healthy_twin() -> (String, BoxView) {
boxed(
"twin",
Some(1_000),
&[
("serving", "no"),
("data", "ok"),
("serves_at", "10.6.0.12:50051"),
("primary", "live"),
("complete", "yes"),
],
)
}
fn codes(v: &EstateView) -> Vec<String> {
GUARDIAN.diagnose(v).into_iter().map(|f| f.code).collect()
}
fn order_kinds(steps: &[Step]) -> Vec<String> {
steps
.iter()
.map(|s| match s {
Step::Cloud(CloudStep::Fence { role }) => format!("fence {role}"),
Step::Cloud(CloudStep::Lay { role }) => format!("lay {role}"),
Step::Cloud(CloudStep::Restore { role }) => format!("restore {role}"),
Step::Order { role, kind, .. } => format!("{kind}→{role}"),
Step::Await { role, what, .. } => format!("await {role} {what}"),
})
.collect()
}
/// The ops the ONE box calls answer what this guardian says, across the guest ABI.
#[test]
fn the_guardian_ops_answer_across_the_abi() {
use monetize_product_guest::{dispatch, ops, wire, Answer, NoHost};
let held = view(vec![
boxed(
"main",
Some(1_000),
&[
("serving", "no"),
("data", "fresh"),
("serves_at", "10.6.0.10:50051"),
],
),
healthy_twin(),
]);
let raw = dispatch(
&crate::HolgerGuest,
&NoHost,
ops::GUARDIAN_DIAGNOSE,
&wire::encode(&held),
);
let found: Answer<Vec<Finding>> = wire::decode(&raw).unwrap();
assert_eq!(found.unwrap(), GUARDIAN.diagnose(&held));
let req = wire::GuardianPlanReq {
remedy: Remedy::RechargeFromTwin,
view: held.clone(),
};
let raw = dispatch(
&crate::HolgerGuest,
&NoHost,
ops::GUARDIAN_PLAN,
&wire::encode(&req),
);
let planned: Answer<Result<Vec<Step>, String>> = wire::decode(&raw).unwrap();
assert_eq!(
order_kinds(&planned.unwrap().unwrap()),
vec![
"recharge_from_twin→main",
"await main recharged",
"restate_entitlements→main"
]
);
let req = wire::GuardianObserveReq {
role: "main".into(),
what: "recharged".into(),
view: held,
};
let raw = dispatch(
&crate::HolgerGuest,
&NoHost,
ops::GUARDIAN_OBSERVE,
&wire::encode(&req),
);
let seen: Answer<StepState> = wire::decode(&raw).unwrap();
assert!(matches!(seen.unwrap(), StepState::Waiting(_)));
}
#[test]
fn a_healthy_estate_has_no_finding() {
assert!(codes(&view(vec![healthy_main(), healthy_twin()])).is_empty());
assert!(
codes(&view(vec![healthy_main()])).is_empty(),
"an estate with no twin at all is not a lost twin"
);
}
#[test]
fn a_held_main_is_no_finding_of_its_own() {
// Gunnar's rule (W3 parity): a re-laid main that reports itself held is the rebuild
// plan's box; its recharge step fills it. No separate finding, auto or not.
for data in ["fresh", "restoring"] {
let main = boxed("main", Some(1_000), &[("serving", "no"), ("data", data)]);
assert!(
codes(&view(vec![main, healthy_twin()])).is_empty(),
"{data}"
);
}
// holger has no `diverged`: a main holding its own state is never a recharge case.
assert!(codes(&view(vec![
boxed("main", Some(1_000), &[("serving", "yes"), ("data", "ok")]),
healthy_twin()
]))
.is_empty());
}
#[test]
fn a_lost_main_is_rebuilt_automatically_only_from_a_whole_blind_twin() {
let silent_main = at(boxed("main", Some(600_000), &[("data", "ok")]), "gone");
let blind_whole = boxed(
"twin",
Some(1_000),
&[("primary", "silent"), ("complete", "yes")],
);
let f = GUARDIAN.diagnose(&view(vec![silent_main.clone(), blind_whole]));
assert_eq!(
(f[0].code.as_str(), f[0].auto),
("main_lost", Some(Remedy::NewServer))
);
assert!(f[0].words.contains("never promoted"), "{}", f[0].words);
assert_eq!(
f[0].remedies,
vec![Remedy::NewServer, Remedy::RechargeFromTwin],
"Gunnar's two buttons (a silent main's recharge is refused at plan time)"
);
let blind_partial = boxed(
"twin",
Some(1_000),
&[
("primary", "silent"),
("complete", "no"),
("why", "3 artifacts failed"),
],
);
let f = GUARDIAN.diagnose(&view(vec![silent_main.clone(), blind_partial]));
assert_eq!((f[0].code.as_str(), f[0].auto), ("main_lost", None));
assert!(f[0].words.contains("3 artifacts failed"), "{}", f[0].words);
assert_eq!(
GUARDIAN.diagnose(&view(vec![silent_main, healthy_twin()]))[0].auto,
None,
"the twin still sees the primary"
);
}
/// ★ **Row 5 (lane G11): silence alone never lays a main again** (it did, at this file's
/// `main_lost`, before). The guard (Gunnar's `rebuild::judge`) reads the provider: a main GONE
/// is laid again by auto-fix; a standing one is booted again first, a stopped one started,
/// and an unasked provider is no evidence.
#[test]
fn auto_fix_lays_a_silent_main_again_only_when_the_provider_says_it_is_gone() {
let whole = || {
boxed(
"twin",
Some(1_000),
&[("primary", "silent"), ("complete", "yes")],
)
};
for (word, auto, said) in [
("gone", Some(Remedy::NewServer), "GONE"),
("active", None, "booted once more"),
("stopped", None, "start it"),
("", None, "not asked"),
] {
let f = GUARDIAN.diagnose(&view(vec![
at(boxed("main", Some(600_000), &[]), word),
whole(),
]));
assert_eq!(
(f[0].code.as_str(), f[0].auto),
("main_lost", auto),
"{word:?}: {}",
f[0].words
);
assert!(f[0].words.contains(said), "{word:?}: {}", f[0].words);
assert_eq!(
f[0].remedies,
vec![Remedy::NewServer, Remedy::RechargeFromTwin],
"the buttons stay"
);
}
}
#[test]
fn a_silent_twin_is_twin_lost_and_the_main_keeps_serving() {
let f = GUARDIAN.diagnose(&view(vec![
healthy_main(),
boxed("twin", Some(600_000), &[]),
]));
assert_eq!(f.len(), 1);
assert_eq!(
(f[0].code.as_str(), f[0].role.as_str(), f[0].auto),
("twin_lost", "twin", Some(Remedy::NewTwin))
);
let main = boxed(
"main",
Some(1_000),
&[("serving", "yes"), ("data", "ok"), ("twin", "silent")],
);
assert_eq!(codes(&view(vec![main])), vec!["twin_lost"]);
}
#[test]
fn both_silent_is_for_a_human() {
let f = GUARDIAN.diagnose(&view(vec![
boxed("main", Some(600_000), &[]),
boxed("twin", Some(600_000), &[]),
]));
assert_eq!(
(f.len(), f[0].code.as_str(), f[0].auto),
(1, "both_lost", None)
);
}
#[test]
fn new_twin_is_laid_told_it_is_the_twin_and_adopted() {
let v = view(vec![healthy_main(), boxed("twin", Some(600_000), &[])]);
let steps = GUARDIAN.plan(Remedy::NewTwin, &v).unwrap();
assert_eq!(
order_kinds(&steps),
vec![
"fence twin",
"lay twin",
"await twin heard",
"become_twin→twin",
"adopt_twin→main",
"await main paired"
]
);
let Step::Order { body, .. } = &steps[3] else {
panic!()
};
assert_eq!(body.get("primary").map(String::as_str), Some("$addr:main"));
assert!(GUARDIAN
.plan(
Remedy::NewTwin,
&view(vec![boxed("main", Some(600_000), &[]), healthy_twin()])
)
.is_err());
}
#[test]
fn new_server_is_gunnars_rebuild_fence_lay_heard_recharge_restate() {
let blind = || {
boxed(
"twin",
Some(1_000),
&[
("serving", "no"),
("serves_at", "10.6.0.12:50051"),
("primary", "silent"),
("complete", "yes"),
],
)
};
let v = view(vec![at(boxed("main", Some(600_000), &[]), "gone"), blind()]);
let steps = GUARDIAN.plan(Remedy::NewServer, &v).unwrap();
assert_eq!(
order_kinds(&steps),
vec![
"fence main",
"lay main",
"await main heard",
"recharge_from_twin→main",
"await main recharged",
"restate_entitlements→main"
]
);
let Step::Order { body, .. } = &steps[3] else {
panic!()
};
assert_eq!(
(
body["main"].as_str(),
body["twin"].as_str(),
body["epoch"].as_str(),
body["accept_lost"].as_str()
),
("$addr:main", "$addr:twin", "$epoch", "false")
);
let Step::Order { body, .. } = &steps[5] else {
panic!()
};
assert_eq!(
body.get("snapshot").map(String::as_str),
Some("$entitlements")
);
assert!(GUARDIAN
.plan(
Remedy::NewServer,
&view(vec![
boxed("main", Some(600_000), &[]),
boxed("twin", Some(600_000), &[])
])
)
.is_err());
// ★ The guard (lane G11): a person's New server destroys no standing disk, lays no
// second main beside one the twin still hears, and leaves a partial twin to a person.
let standing = GUARDIAN
.plan(
Remedy::NewServer,
&view(vec![
at(boxed("main", Some(600_000), &[]), "active"),
blind(),
]),
)
.unwrap_err();
assert!(standing.contains("booted once more"), "{standing}");
let heard = GUARDIAN
.plan(
Remedy::NewServer,
&view(vec![
at(boxed("main", Some(600_000), &[]), "gone"),
healthy_twin(),
]),
)
.unwrap_err();
assert!(heard.contains("still hears its primary"), "{heard}");
let partial = boxed(
"twin",
Some(1_000),
&[
("primary", "silent"),
("complete", "no"),
("why", "3 artifacts failed"),
],
);
let partial = GUARDIAN
.plan(
Remedy::NewServer,
&view(vec![at(boxed("main", Some(600_000), &[]), "gone"), partial]),
)
.unwrap_err();
assert!(partial.contains("3 artifacts failed"), "{partial}");
// An operator's estate (monetize laid no main: no provider word): the twin's half of the
// guard decides here, and the operator's kit confirms before it destroys anything.
assert!(GUARDIAN
.plan(
Remedy::NewServer,
&view(vec![boxed("main", Some(600_000), &[]), blind()])
)
.is_ok());
}
#[test]
fn recharge_runs_only_on_a_fresh_or_restoring_main() {
let fresh = boxed("main", Some(1_000), &[("serving", "no"), ("data", "fresh")]);
let steps = GUARDIAN
.plan(Remedy::RechargeFromTwin, &view(vec![fresh, healthy_twin()]))
.unwrap();
assert_eq!(
order_kinds(&steps),
vec![
"recharge_from_twin→main",
"await main recharged",
"restate_entitlements→main"
]
);
let serving = boxed(
"main",
Some(1_000),
&[("serving", "yes"), ("data", "restoring")],
);
let steps = GUARDIAN
.plan(
Remedy::RechargeFromTwin,
&view(vec![serving, healthy_twin()]),
)
.unwrap();
assert_eq!(order_kinds(&steps)[0], "fence_primary→main");
let holding = GUARDIAN
.plan(
Remedy::RechargeFromTwin,
&view(vec![healthy_main(), healthy_twin()]),
)
.unwrap_err();
assert!(
holding.contains("FRESH") && holding.contains("New server"),
"{holding}"
);
assert!(GUARDIAN
.plan(
Remedy::RechargeFromTwin,
&view(vec![
boxed("main", Some(1_000), &[("data", "fresh")]),
boxed("twin", Some(600_000), &[])
])
)
.is_err());
}
#[test]
fn review_twin_and_restore_front_are_gunnars_shape() {
assert_eq!(
order_kinds(
&GUARDIAN
.plan(
Remedy::ReviewTwin,
&view(vec![healthy_main(), healthy_twin()])
)
.unwrap()
),
vec!["review_twin→main"]
);
let steps = GUARDIAN
.plan(
Remedy::RestoreFront,
&view(vec![boxed("main", Some(600_000), &[]), healthy_twin()]),
)
.unwrap();
assert_eq!(
order_kinds(&steps),
vec!["restore front", "await twin heard"]
);
}
#[test]
fn observe_reads_paired_and_recharged_from_the_report() {
let v = view(vec![healthy_main(), healthy_twin()]);
assert!(matches!(
GUARDIAN.observe("main", "paired", &v),
StepState::Met(_)
));
assert!(matches!(
GUARDIAN.observe("main", "recharged", &v),
StepState::Met(_)
));
let v = view(vec![boxed(
"main",
Some(1_000),
&[("data", "restoring"), ("twin", "none")],
)]);
assert!(matches!(
GUARDIAN.observe("main", "paired", &v),
StepState::Waiting(_)
));
assert!(matches!(
GUARDIAN.observe("main", "recharged", &v),
StepState::Waiting(_)
));
let v = view(vec![boxed(
"main",
Some(1_000),
&[("data", "restoring"), ("recharge", "failed")],
)]);
assert!(matches!(
GUARDIAN.observe("main", "recharged", &v),
StepState::Failed(_)
));
assert!(matches!(
GUARDIAN.observe("main", "dancing", &v),
StepState::Failed(_)
));
// ★ Owner 2026-11-03: Gunnar's rule for Holger too — a main that reports its data whole
// is recharged, whatever an older `recharge` word still says.
let v = view(vec![boxed(
"main",
Some(1_000),
&[("data", "ok"), ("recharge", "failed")],
)]);
assert!(matches!(
GUARDIAN.observe("main", "recharged", &v),
StepState::Met(_)
));
}
}