Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-admin/src/self_update.rs

//! **The ONE box updates ITSELF** (owner 2026-10-14, point 5: "the ONE box upgrades itself from
//! the next signed release (A/B)"; bootstrapping an EMPTY box stays the kit's).
//!
//! The mechanism is korp's and generic (tunnr `korp_installer::live`): two app slots on the
//! system disk, the slot setting on the ESP, korp-start's own checks at every start, a trial
//! that rolls back when the new app never confirms. What an appliance adds is its app's half —
//! and this is the ONE box's:
//!
//! ```text
//!   operator ── POST /self/update (door, mTLS) / `monetize box update --apply`
//!     the release bucket: release/monetize-admin/boxes/<host>/next.live.toml   (the record: BASE, pin, len, sig, archive key;
//!                         this box's own; the shared release/monetize-admin/next.live.toml only while none is written)
//!     ──▶ decide: same pin → nothing; another BASE / live off → the kit's keep upgrade (said, refused)
//!     ──▶ the archive fetched (presigned with the box's own READ pair), sha256 + length + pin proven
//!     ──▶ korp `stage_on`: the signature against the ENROLLED root, the rollback floor, the BASE,
//!         written to the slot NOT running, `<slot> trial` last
//!     ──▶ the children stopped (SIGTERM, then SIGKILL), synced, PID 1 re-execs korp-start (/init)
//!   the new app: serves 60 s (monetize-server answers) ──▶ confirms its slot
//!                never serves by 600 s ──▶ re-execs korp-start, which rolls back to the old slot
//! ```
//!
//! `/data` is never touched: the same mounts ride through the re-exec. The signing key never
//! reaches the box: it only VERIFIES under the root it enrolled at its install.

use std::time::Duration;

/// Where the next release of this box is recorded in the release bucket (written by the
/// release build next to the archive it names).
pub const NEXT_KEY: &str = "release/monetize-admin/next.live.toml";
/// **This box's OWN pointer** (M-FOLLOW 4, R34-15): `release/monetize-admin/boxes/<host>/next.live.toml`,
/// `host` the name the box answers (its fed `web_hosts`, the kit's `[boxes.<b>] host`).
/// `None` for a host that cannot name an object (then only [`NEXT_KEY`] is read).
pub fn next_key_of(host: &str) -> Option<String> {
    let h = host.trim().to_ascii_lowercase();
    let ok = !h.is_empty() && h.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') && !h.starts_with(['.', '-']) && !h.contains("..");
    ok.then(|| format!("release/monetize-admin/boxes/{h}/next.live.toml"))
}

/// **The next-release record this box reads** (M-FOLLOW 4): its own pointer ([`next_key_of`])
/// when the bucket holds it; only when the bucket answers it has none (`Ok(None)`, a 404: a kit
/// that has not written it yet — today's) the shared [`NEXT_KEY`]. Any other answer is the
/// box's error: a bucket that fails is never read past into the shared pointer. Answers the
/// key read and its bytes; `fetch` is the bucket (`Ok(None)`: not there).
pub fn read_next(host: &str, fetch: impl Fn(&str) -> Result<Option<Vec<u8>>, String>) -> Result<(String, Vec<u8>), String> {
    if let Some(own) = next_key_of(host) {
        if let Some(b) = fetch(&own)? {
            return Ok((own, b));
        }
    }
    fetch(NEXT_KEY)?.map(|b| (NEXT_KEY.to_string(), b)).ok_or_else(|| format!("[self-next] the bucket holds no next release for this box ({}, nor {NEXT_KEY})", next_key_of(host).unwrap_or_else(|| "no own pointer".into())))
}

/// How long the new app serves before it confirms its slot.
pub const CONFIRM_AFTER: Duration = Duration::from_secs(60);
/// A trial slot not serving by then is rolled back (korp-start, on the re-exec).
pub const CONFIRM_DEADLINE: Duration = Duration::from_secs(600);
/// How long the children have to stop after SIGTERM.
pub const STOP_GRACE: Duration = Duration::from_secs(20);
/// How long the next release's app has to answer its `server --check` (OXG-4).
pub const ASK_DEADLINE: Duration = Duration::from_secs(120);
/// The largest archive taken (korp's stage ceiling).
pub const MAX_ARCHIVE: usize = 256 * 1024 * 1024;

/// **The next release of this box**, as the bucket records it: korp's live record of the
/// monetize-admin medium, and where its archive is.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Next {
    pub base: String,
    pub app: String,
    pub app_len: u64,
    /// hex of `app.sig`
    pub sig: String,
    /// The archive's key in the bucket (`release/monetize-admin-live/<pin>.live-app.znippy`);
    /// empty for the way back, which is the other slot's own bytes (OXG-3).
    pub object: String,
    pub sha256: String,
    /// The monetize commit it was built from (said, never decided on).
    pub build: String,
}

/// Pure: [`Next`] from its TOML lines (`key = "value"`).
pub fn parse_next(text: &str) -> Result<Next, String> {
    let get = |k: &str| -> Result<String, String> {
        text.lines()
            .find_map(|l| l.split_once('=').filter(|(key, _)| key.trim() == k).map(|(_, v)| v.trim().trim_matches('"').to_string()))
            .ok_or_else(|| format!("[self-next] the record names no `{k}`"))
    };
    let n = Next {
        base: get("base")?,
        app: get("app")?,
        app_len: get("app_len")?.parse().map_err(|e| format!("[self-next] app_len: {e}"))?,
        sig: get("sig")?,
        object: get("object")?,
        sha256: get("sha256")?,
        build: get("build").unwrap_or_default(),
    };
    if !n.object.starts_with("release/") || n.object.contains("..") {
        return Err(format!("[self-next] the archive {:?} is not a release object", n.object));
    }
    Ok(n)
}

/// What this box runs now (korp's words: BASE, app pin, slot and trial, the off switch).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Running {
    pub base: String,
    pub app: String,
    pub slot: String,
    pub live_update: bool,
    /// The app pin the OTHER slot holds (empty: none): a release staged there and not
    /// running — with no trial open — is one korp-start rolled back, unless it confirmed here.
    pub other_app: String,
    /// R34-4: whether the other slot's app is one this box ran CONFIRMED (its record,
    /// [`CONFIRMED_REL`]): then it is the operator's way back, not a rolled-back release.
    pub other_confirmed: bool,
}

/// What the operator's word does ([`decide`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Plan {
    /// It runs the next release's app already.
    Nothing,
    /// The next release's app is staged into the other slot.
    Live,
    /// Not live: the kit's keep upgrade (`cargo xtask upgrade`) — why.
    Base(String),
    /// A trial is still open: confirm it (or let it roll back) first.
    Wait(String),
    /// **The next release is the one this box ROLLED BACK** (owner 2026-10-18): it never
    /// confirmed here, so it is never offered again — only a newer release is.
    RolledBack(String),
    /// **The next release's app refuses this box's fed config** (OXG-4): its own parse said so
    /// before anything was staged — what to feed first.
    Config(String),
}

/// Pure (OXG-4): **the plan once the next release's app has said whether it reads this box's
/// fed config**. Only a [`Plan::Live`] asks; a next feed staged for that app is the config it
/// starts on (its own loader judges it at that start), so it is not asked about the old one.
pub fn plan_with_config(p: Plan, n: &Next, next_feed_staged: bool, check: impl FnOnce() -> Result<(), String>) -> Plan {
    if p != Plan::Live || next_feed_staged {
        return p;
    }
    match check() {
        Ok(()) => Plan::Live,
        Err(why) => {
            let name = if n.build.trim().is_empty() { short(&n.app) } else { &n.build[..n.build.len().min(7)] };
            Plan::Config(format!(
                "[self-next-config] the next release {name} (app {}) refuses this box's fed config, by its own parse: {} — nothing was staged. Feed first: `feed --apply` with a feed both apps read, or `feed --next` (the next release's feed, staged for it); then `box update` again",
                short(&n.app),
                why.trim()
            ))
        }
    }
}

/// Pure (OXG-4): **the next app's word** from its `server --check` output: an `ok: ` line on
/// stdout is yes; else its reason, the `monetize-admin server:` line on stderr (else the last
/// line it said). Silence is never taken for yes.
pub fn check_said(stdout: &str, stderr: &str) -> Result<(), String> {
    if stdout.lines().any(|l| l.trim_start().starts_with("ok: ")) {
        return Ok(());
    }
    const SAID: &str = "monetize-admin server:";
    let lines: Vec<&str> = stderr.lines().map(str::trim).filter(|l| !l.is_empty()).collect();
    match lines.iter().find(|l| l.starts_with(SAID)).or(lines.last()) {
        Some(l) => Err(l.trim_start_matches(SAID).trim().to_string()),
        None => Err("its `server --check` said nothing (no `ok:` line, no reason)".into()),
    }
}

/// Pure: **whether `update` goes on** with the plan: refused by name, else on (a
/// [`Plan::Nothing`] goes on to say there is nothing to do).
pub fn update_words(p: &Plan) -> Result<(), String> {
    match p {
        Plan::Base(why) | Plan::Wait(why) | Plan::RolledBack(why) | Plan::Config(why) => Err(format!("REFUSED {why}")),
        Plan::Live | Plan::Nothing => Ok(()),
    }
}

/// Pure: **what the operator's word does**.
pub fn decide(r: &Running, n: &Next) -> Plan {
    if r.slot.split_whitespace().nth(1).is_some() {
        return Plan::Wait(format!("[live-trial-open] slot {}: the last update is not confirmed yet", r.slot));
    }
    if r.app.trim() == n.app.trim() {
        return Plan::Nothing;
    }
    // The other slot holds the next app, this box runs another and no trial is open: korp-start
    // rolled it back — unless the box ran it confirmed (R34-4: then it is the operator's way
    // back, offered like any release). One not on the record is never offered again.
    if !r.other_app.trim().is_empty() && r.other_app.trim() == n.app.trim() && !r.other_confirmed {
        let name = if n.build.trim().is_empty() { short(&n.app) } else { &n.build[..n.build.len().min(7)] };
        return Plan::RolledBack(format!("rolled back: {name} — not offered again (this box has no record of it confirmed here); a newer release is"));
    }
    if !r.live_update {
        return Plan::Base("live update is OFF on this box (korp.live_update=off, its BASE): the kit's keep upgrade".into());
    }
    if r.base.trim() != n.base.trim() {
        return Plan::Base(format!("the next release runs another BASE ({} → {}): the kit's keep upgrade (`cargo xtask upgrade`)", short(&r.base), short(&n.base)));
    }
    if n.sig.trim().is_empty() {
        return Plan::Base("the next release's app is unsigned: never taken live".into());
    }
    Plan::Live
}

/// Pure: **the app a next feed is staged for** (M-FOLLOW 7, `PUT /feed/next`): the release this
/// box's own pointer names, when the box would take it live ([`Plan::Live`]) — never the app it
/// runs (that feed is `PUT /feed`), one it rolled back, or one it would refuse.
pub fn next_feed_for(r: &Running, n: &Next, p: &Plan) -> Result<String, String> {
    match p {
        // OXG-4: staging the next release's feed is one way to feed it first.
        Plan::Live | Plan::Config(_) => Ok(n.app.trim().to_string()),
        Plan::Nothing => Err(format!("[feed-next-runs] this box runs the next release's app {} already: its feed is put with `feed --apply`", short(&r.app))),
        Plan::Wait(w) | Plan::Base(w) | Plan::RolledBack(w) => Err(format!("[feed-next-not-live] the next release is not one this box takes live, so no feed is staged for it: {w}")),
    }
}

/// The app pin this box runs (empty off a korp box: no staged feed is ever taken there).
pub fn running_app() -> String {
    appliance::running().map(|r| r.app).unwrap_or_default()
}

/// The app pin this box runs and whether its slot is on trial; `None` off a korp box.
pub fn running_trial() -> Option<(String, bool)> {
    appliance::running().ok().map(|r| (r.app.clone(), r.slot.split_whitespace().nth(1).is_some()))
}

/// Pure: **the operator's way back** (M-FOLLOW 3, R34-4) — the release the OTHER slot holds,
/// as its korp records name it (`other_len`, `other_sig` hex), when it ran confirmed here.
/// The record a stage needs is built from the box's own records, never from the bucket: its
/// app pin, length and signature from the slot, the BASE the box runs (a live update never
/// changes it, so both slots run it). The archive is the slot's own bytes ([`way_back`],
/// OXG-3): no bucket object is named. Refused by name while a trial is open: the confirm
/// window is korp-start's own rollback.
pub fn decide_back(r: &Running, other_len: u64, other_sig: &str) -> Result<Next, String> {
    if r.slot.split_whitespace().nth(1).is_some() {
        return Err(format!("[live-trial-open] slot {}: the last update is not confirmed yet — inside the confirm window the box rolls back by itself", r.slot));
    }
    let other = r.other_app.trim();
    if other.is_empty() {
        return Err("[self-back-none] the other slot holds no app: this box has no way back".into());
    }
    if !r.other_confirmed {
        return Err(format!("[self-back-unconfirmed] the other slot's app {} is not on this box's record of confirmed releases (korp-start rolled it back, or it ran before the box kept the record): it is never taken back", short(other)));
    }
    if !r.live_update {
        return Err("live update is OFF on this box (korp.live_update=off, its BASE): the kit's keep upgrade".into());
    }
    if other_sig.trim().is_empty() {
        return Err(format!("[self-unsigned] the other slot's app {} carries no signature: never taken live", short(other)));
    }
    Ok(Next { base: r.base.trim().to_string(), app: other.to_string(), app_len: other_len, sig: other_sig.trim().to_string(), object: String::new(), sha256: String::new(), build: String::new() })
}

/// Pure: **the way back from the box's OWN other slot** (OXG-3) — [`decide_back`] on the slot's
/// records, then the slot's own bytes (`read_slot(len)`) proven by their length and pin. The
/// bucket is never asked.
pub fn way_back(r: &Running, len: u64, sig: &str, read_slot: impl FnOnce(u64) -> Result<Vec<u8>, String>) -> Result<(Next, Vec<u8>), String> {
    let n = decide_back(r, len, sig)?;
    let archive = read_slot(n.app_len).map_err(|e| format!("[self-back-slot] the other slot's app {} cannot be read from its slot: {e}", short(&n.app)))?;
    prove(&archive, &n).map_err(|e| format!("[self-back-slot] the other slot does not hold its app {} whole: {}", short(&n.app), e.trim_start_matches("[self-fetch] ")))?;
    Ok((n, archive))
}

/// Pure: **the way back in the operator's words** (`box running`); `back` is whether the box
/// can take it now ([`way_back`] and the slot verified).
pub fn back_words<T>(r: &Running, back: &Result<T, String>) -> String {
    let other = r.other_app.trim();
    if other.is_empty() {
        "no way back: the other slot holds no app".into()
    } else if r.slot.split_whitespace().nth(1).is_some() {
        format!("no way back yet: slot {} is on trial (korp-start rolls it back by itself inside the confirm window)", r.slot)
    } else if !r.other_confirmed {
        format!("no way back: the other slot's app {} is not on this box's record of confirmed releases", short(other))
    } else if let Err(e) = back {
        format!("no way back: {}", e.trim_start_matches("REFUSED "))
    } else {
        format!("app {} in the other slot, confirmed on this box: `monetize box update --back --apply`", short(other))
    }
}

/// **At the confirm, the app the trial came from goes on the record** (M-FOLLOW 3): an update
/// is refused while a trial is open, so the app in the slot left behind ran CONFIRMED when the
/// update was taken. This heals a box updated by an older app that kept no record. Called only
/// by the trial watcher once its slot is confirmed — never inside the confirm window.
pub fn confirmed_from(root: &std::path::Path, r: &Running) {
    if r.slot.split_whitespace().nth(1).is_none() {
        record_confirmed(root, r.other_app.trim());
    }
}

fn record_confirmed(root: &std::path::Path, app: &str) {
    if app.is_empty() {
        return;
    }
    let p = root.join(CONFIRMED_REL);
    let mut known: Vec<String> = std::fs::read_to_string(&p).unwrap_or_default().lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect();
    if known.iter().any(|k| k == app) {
        return;
    }
    known.push(app.to_string());
    let _ = p.parent().map(std::fs::create_dir_all);
    if let Err(e) = std::fs::write(&p, known.join("\n") + "\n") {
        tracing::warn!(target: "monetize_admin::self_update", "self update: the confirmed app {} could not be recorded at {}: {e}", short(app), p.display());
    }
}

/// Pure: **the door's ask** (`POST /self/update`): an empty body or `{}` is the next release,
/// `{"back": true}` the way back ([`update_back`]).
pub fn ask_of(body: &str) -> Result<bool, String> {
    if body.trim().is_empty() {
        return Ok(false);
    }
    let v: serde_json::Value = serde_json::from_str(body).map_err(|e| format!("[self-ask] {{}} or {{\"back\": true}}: {e}"))?;
    match (v.is_object(), &v["back"]) {
        (true, serde_json::Value::Null) => Ok(false),
        (true, serde_json::Value::Bool(b)) => Ok(*b),
        _ => Err("[self-ask] {} takes the next release, {\"back\": true} the other slot's confirmed one".into()),
    }
}

fn short(h: &str) -> &str {
    &h[..h.len().min(12)]
}

/// Pure: **the release bucket's base** (`https://<endpoint>/<bucket>`) from any release
/// object's url this box is configured with (`…/release/<kind>/<file>`).
pub fn bucket_base(object_url: &str) -> Option<String> {
    let u = object_url.split('?').next()?;
    let i = u.rfind("/release/")?;
    (u.starts_with("https://") || bench_loopback(u)).then(|| u[..i].to_string())
}

/// The bench's bucket (the mock's `/objsto` on the host's loopback): `http://127.0.0.1:<port>/…`.
fn bench_loopback(u: &str) -> bool {
    u.starts_with("http://127.0.0.1:")
}

/// **Where a box reaches a loopback url of the bench**: a guest's own loopback holds no bucket;
/// the mock's guestfwd answers the host's ports at 10.0.2.100. Any other url is as given.
pub fn reachable(url: &str) -> String {
    match url.strip_prefix("http://127.0.0.1:") {
        Some(rest) => format!("http://10.0.2.100:{rest}"),
        None => url.to_string(),
    }
}

/// Pure: **the archive proven** — its sha256, length and pin as the record says.
pub fn prove(archive: &[u8], n: &Next) -> Result<(), String> {
    if archive.len() as u64 != n.app_len {
        return Err(format!("[self-fetch] the archive is {} bytes, the record says {}", archive.len(), n.app_len));
    }
    let sha = ring::digest::digest(&ring::digest::SHA256, archive);
    let sha: String = sha.as_ref().iter().map(|b| format!("{b:02x}")).collect();
    // The way back's record is the slot's own ([`decide_back`]): no sha256; its pin proves it.
    if !n.sha256.trim().is_empty() && sha != n.sha256.trim() {
        return Err(format!("[self-fetch] the archive's sha256 is {}, the record says {}", short(&sha), short(&n.sha256)));
    }
    let pin = blake3::hash(archive).to_hex().to_string();
    if pin != n.app.trim() {
        return Err(format!("[self-fetch] the archive's pin is {}, the record says {}", short(&pin), short(&n.app)));
    }
    Ok(())
}

/// Pure: hex → bytes.
pub fn unhex(s: &str) -> Result<Vec<u8>, String> {
    let s = s.trim();
    if s.len() % 2 != 0 {
        return Err("odd hex".into());
    }
    (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())).collect()
}

/// What the confirm watcher does next.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConfirmStep {
    Nothing,
    Wait,
    Confirm,
    RollBack,
}

/// Pure: **the trial watcher's step** — on trial, how long PID 1 has run, since when it serves.
pub fn confirm_plan(on_trial: bool, up: Duration, serving_since: Option<Duration>) -> ConfirmStep {
    if !on_trial {
        return ConfirmStep::Nothing;
    }
    match serving_since {
        Some(since) if up.saturating_sub(since) >= CONFIRM_AFTER => ConfirmStep::Confirm,
        Some(_) => ConfirmStep::Wait,
        None if up > CONFIRM_DEADLINE => ConfirmStep::RollBack,
        None => ConfirmStep::Wait,
    }
}

// ── the box's side ──────────────────────────────────────────────────────────

/// The bucket's READ pair and its base, from monetize's own configuration on this box.
fn bucket(root: &std::path::Path) -> Result<(String, monetize_server::estate_order::ObjstoKey), String> {
    let cfg = monetize_server::config::Config::load(&root.join(monetize_front::intake::CONFIG_REL)).map_err(|e| format!("[self-no-config] {e:#}"))?;
    let orders = cfg.estate_orders.as_ref().ok_or("[self-no-bucket] this box names no release bucket ([estate_orders] is absent)")?;
    let url = orders
        .media_objects
        .values()
        .map(|o| o.url.clone())
        .chain(cfg.estates.values().flat_map(|e| e.orders.media_objects.values().map(|o| o.url.clone())))
        .find_map(|u| bucket_base(&u))
        .ok_or("[self-no-bucket] no release object names the bucket (`[estate_orders.media_objects]`)")?;
    let key_file = cfg.secrets_dir().join(monetize_server::estate_order::MEDIA_OBJSTO_FED);
    let key: monetize_server::estate_order::ObjstoKey = serde_json::from_slice(&std::fs::read(&key_file).map_err(|_| "[self-no-bucket-key] the bucket's READ pair is not fed (secret.media_objsto)".to_string())?).map_err(|e| format!("[self-no-bucket-key] the READ pair: {e}"))?;
    Ok((url, key))
}

/// One object of the release bucket, presigned with the box's READ pair and fetched.
fn get(base: &str, key: &monetize_server::estate_order::ObjstoKey, object: &str, cap: usize) -> Result<Vec<u8>, String> {
    get_opt(base, key, object, cap)?.ok_or_else(|| format!("[self-fetch] {object}: the bucket answered 404 Not Found"))
}

/// [`get`], `Ok(None)` when the bucket answers it holds no such object (404).
fn get_opt(base: &str, key: &monetize_server::estate_order::ObjstoKey, object: &str, cap: usize) -> Result<Option<Vec<u8>>, String> {
    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
    let url = monetize_server::estate_order::presign_get(&format!("{base}/{object}"), key, now, 600)?;
    let _ = rustls::crypto::ring::default_provider().install_default();
    let client = reqwest::blocking::Client::builder()
        .tls_certs_only(webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().filter_map(|c| reqwest::Certificate::from_der(c).ok()))
        .connect_timeout(Duration::from_secs(30))
        .timeout(Duration::from_secs(600))
        .build()
        .map_err(|e| format!("[self-fetch] no HTTPS client: {e}"))?;
    let resp = client.get(reachable(&url)).send().map_err(|e| format!("[self-fetch] {object}: {e}"))?;
    if resp.status() == reqwest::StatusCode::NOT_FOUND {
        return Ok(None);
    }
    if !resp.status().is_success() {
        return Err(format!("[self-fetch] {object}: the bucket answered {}", resp.status()));
    }
    if resp.content_length().is_some_and(|n| n as usize > cap) {
        return Err(format!("[self-fetch] {object} is larger than {cap} bytes"));
    }
    let bytes = resp.bytes().map_err(|e| format!("[self-fetch] {object}: {e}"))?;
    if bytes.len() > cap {
        return Err(format!("[self-fetch] {object} is larger than {cap} bytes"));
    }
    Ok(Some(bytes.to_vec()))
}

/// Where the box keeps every app it ran CONFIRMED (one app pin a line): the way back the
/// operator may take (R34-4).
pub const CONFIRMED_REL: &str = "solo/self-update.confirmed";

/// **The box's record of confirmed apps, kept and read**: `r.app` is noted when no trial is
/// open (it serves confirmed), and `r.other_confirmed` says whether the other slot's app is
/// on the record.
pub fn known_confirmed(root: &std::path::Path, r: &mut Running) {
    if r.slot.split_whitespace().nth(1).is_none() {
        record_confirmed(root, r.app.trim());
    }
    let known: Vec<String> = std::fs::read_to_string(root.join(CONFIRMED_REL)).unwrap_or_default().lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect();
    let other = r.other_app.trim();
    r.other_confirmed = !other.is_empty() && known.iter().any(|k| k == other);
}

/// **What this box runs and what the bucket offers**, and what the operator's word would do.
pub fn look(root: &std::path::Path) -> Result<(Running, Next, Plan), String> {
    look_from(root).map(|(r, n, p, _)| (r, n, p))
}

/// [`look`], with the pointer the record was read from ([`read_next`]: this box's own, or the
/// shared one while the kit writes none).
pub fn look_from(root: &std::path::Path) -> Result<(Running, Next, Plan, String), String> {
    let mut r = appliance::running()?;
    known_confirmed(root, &mut r);
    let (base, key) = bucket(root)?;
    let host = crate::solo::UiSettings::read(root).map(|u| u.rp_id).unwrap_or_default();
    let (from, text) = read_next(&host, |k| get_opt(&base, &key, k, 64 * 1024))?;
    let n = parse_next(&String::from_utf8_lossy(&text))?;
    let p = decide(&r, &n);
    Ok((r, n, p, from))
}

/// [`look_from`] with **the next release's app asked about this box's fed config** (OXG-4):
/// `box running` and the plan of `box update` say [`Plan::Config`] before anything is staged.
pub fn look_checked(root: &std::path::Path) -> Result<(Running, Next, Plan, String), String> {
    let (r, n, p, from) = look_from(root)?;
    let p = plan_with_config(p, &n, next_feed_staged_for(root, &n), || next_app_reads_config(root, &n, None));
    Ok((r, n, p, from))
}

/// Whether a next feed is staged for the next release's app (`PUT /feed/next`): that app starts
/// on it, so the config it reads is that feed's, judged by its own loader at its start.
fn next_feed_staged_for(root: &std::path::Path, n: &Next) -> bool {
    crate::solo::read_next_feed(root).is_some_and(|s| s.for_app.trim() == n.app.trim())
}

/// The next app's last word on a config: (app pin, blake3 of the config, its word). `box
/// running` is looked at every few seconds; the archive is fetched and the app asked once per
/// release and config.
static ASKED: std::sync::Mutex<Option<Asked>> = std::sync::Mutex::new(None);

/// One word of [`ASKED`]: the app pin, the config's blake3, the app's word.
type Asked = (String, String, Result<(), String>);

/// **The next release's app asked about this box's fed config** (OXG-4): its own entry, out of
/// its archive (the update's in hand, else fetched and proven), verified as korp's stage verifies
/// it, runs `server --check --config <the box's config>` — the start's own checks, starting
/// nothing. `Err` is the app's own refusal. What cannot be asked (no archive, a box that is not
/// korp's, an exec that fails) is said in the log and lets the plan stand: the stage refuses
/// what does not verify, and the trial's rollback stays the last guard.
fn next_app_reads_config(root: &std::path::Path, n: &Next, archive: Option<&[u8]>) -> Result<(), String> {
    let cfg = root.join(monetize_front::intake::CONFIG_REL);
    let unasked = |why: String| {
        tracing::warn!(target: "monetize_admin::self_update", app = %n.app, "self update: the next release's app could not be asked about this box's config ({why}); the plan stands");
        Ok(())
    };
    let digest = match std::fs::read(&cfg) {
        Ok(t) => blake3::hash(&t).to_hex().to_string(),
        Err(e) => return unasked(format!("{}: {e}", cfg.display())),
    };
    if let Some((app, d, word)) = ASKED.lock().ok().and_then(|g| g.clone()) {
        if app == n.app.trim() && d == digest {
            return word;
        }
    }
    let fetched;
    let archive = match archive {
        Some(a) => a,
        None => {
            fetched = match bucket(root).and_then(|(base, key)| get(&base, &key, &n.object, MAX_ARCHIVE)).and_then(|a| prove(&a, n).map(|_| a)) {
                Ok(a) => a,
                Err(e) => return unasked(e),
            };
            &fetched[..]
        }
    };
    match appliance::ask_next_app(archive, n, &cfg) {
        Ok(word) => {
            if let Ok(mut g) = ASKED.lock() {
                *g = Some((n.app.trim().to_string(), digest, word.clone()));
            }
            word
        }
        Err(e) => unasked(e),
    }
}

/// **The operator's word**: the next release staged into the other slot, then — after the
/// answer left — the children stopped and PID 1 re-exec'd into it. `Err` names why not.
pub fn update(root: &std::path::Path) -> Result<String, String> {
    let (r, n, p) = look(root)?;
    update_words(&p)?;
    if p == Plan::Nothing {
        return Ok(format!("this box runs app {} already: nothing to do", short(&r.app)));
    }
    let (base, key) = bucket(root)?;
    let archive = get(&base, &key, &n.object, MAX_ARCHIVE)?;
    prove(&archive, &n)?;
    // OXG-4: the next app reads this box's fed config, or nothing is staged.
    update_words(&plan_with_config(p, &n, next_feed_staged_for(root, &n), || next_app_reads_config(root, &n, Some(&archive))))?;
    stage_and_restart(&archive, &n, "the next release")
}

/// **The way back as this box can take it now** (OXG-3): [`way_back`] on the box's OWN other
/// slot — its korp records and its partition's bytes, never the bucket (an app laid by install
/// or upgrade was never published there; only `self-release` publishes) — and those bytes
/// verified with korp-start's own legs (the signature under the enrolled root, the rollback
/// floor, the BASE). `box running` offers `--back` only when this is `Ok`.
pub fn back_on_box(r: &Running) -> Result<(Next, Vec<u8>), String> {
    let (len, sig) = if r.other_app.trim().is_empty() { (0, String::new()) } else { appliance::other_records()? };
    let (n, archive) = way_back(r, len, &sig, appliance::read_other_slot)?;
    appliance::verify_other(&archive, &n).map_err(|e| format!("[self-back-verify] the other slot's app {} does not verify on this box: {e}", short(&n.app)))?;
    Ok((n, archive))
}

/// [`back_on_box`] for the box's running word (`GET /self/running`): only whether it can be
/// taken.
pub fn back_now(r: &Running) -> Result<(), String> {
    back_on_box(r).map(|_| ())
}

/// **The operator's way back** (M-FOLLOW 3, R34-4, OXG-3): the other slot's release that
/// confirmed on this box, taken from that slot itself ([`back_on_box`]) — the slot still
/// holds it, so nothing is fetched or written but its records and the setting — and restarted
/// into exactly as [`update`] does: on trial, so korp-start still rolls it back by itself if it
/// never serves.
pub fn update_back(root: &std::path::Path) -> Result<String, String> {
    let mut r = appliance::running()?;
    known_confirmed(root, &mut r);
    let (n, archive) = back_on_box(&r).map_err(|e| format!("REFUSED {e}"))?;
    let sig = unhex(&n.sig).map_err(|e| format!("[self-unsigned] the signature: {e}"))?;
    let slot = appliance::stage_other_in_place(&archive, &n.app, &sig, &n.base)?;
    restart(&n, slot, archive.len(), "the way back")
}

fn stage_and_restart(archive: &[u8], n: &Next, what: &str) -> Result<String, String> {
    let sig = unhex(&n.sig).map_err(|e| format!("[self-unsigned] the signature: {e}"))?;
    let slot = appliance::stage(archive, &n.app, &sig, &n.base)?;
    restart(n, slot, archive.len(), what)
}

/// The slot staged: said, then — after the answer left — the children stopped and PID 1
/// re-exec'd into it.
fn restart(n: &Next, slot: String, len: usize, what: &str) -> Result<String, String> {
    tracing::warn!(target: "monetize_admin::self_update", app = %n.app, build = %n.build, "self update: {what} is staged in slot {slot} — the children stop and PID 1 re-execs into it");
    // The restart waits for the answer to leave; a box that cannot start the thread says so
    // and keeps running the release it runs (the staged slot waits for the next update).
    named("self-update-reexec", || {
        std::thread::sleep(Duration::from_secs(2));
        let e = appliance::stop_children_and_reexec();
        tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.reexec the re-exec FAILED (the children are stopped): {e}");
    })
    .map_err(|e| format!("staged into slot {slot}, but this box cannot restart into it now: {e}"))?;
    let name = if n.build.is_empty() && what == "the way back" { "(the way back)" } else if n.build.is_empty() { "(unnamed)" } else { &n.build };
    Ok(format!("staged release {name} (app {}, {len} bytes) into slot {slot}; this box restarts into it now and confirms after {} s of serving, else rolls back", short(&n.app), CONFIRM_AFTER.as_secs()))
}

/// **The console's word** (owner 2026-10-18, "Update this box"): the console (a child of PID 1)
/// leaves this marker — who asked — and PID 1 takes it ([`keep_console_asks`]); only PID 1 can
/// stop the children and re-exec.
pub const ASK_REL: &str = "solo/self-update.ask";
/// The box's last word on an update the console asked (staged …, or REFUSED …).
pub const LAST_REL: &str = "solo/self-update.last";

/// Leave the console's word for PID 1; refused while one waits.
pub fn ask(root: &std::path::Path, by: &str) -> Result<(), String> {
    let p = root.join(ASK_REL);
    if p.exists() {
        return Err("an update of this box is asked already: the box takes it within seconds".into());
    }
    if let Some(d) = p.parent() {
        std::fs::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?;
    }
    std::fs::write(&p, by.trim()).map_err(|e| format!("{}: {e}", p.display()))
}

/// Whether a word waits.
pub fn asked(root: &std::path::Path) -> bool {
    root.join(ASK_REL).exists()
}

/// Take the waiting word (who asked), if any: it is gone once taken.
pub fn take_ask(root: &std::path::Path) -> Option<String> {
    let p = root.join(ASK_REL);
    let by = std::fs::read_to_string(&p).ok()?;
    std::fs::remove_file(&p).ok()?;
    Some(by.trim().to_string())
}

/// The box's last word on a console-asked update.
pub fn last(root: &std::path::Path) -> String {
    std::fs::read_to_string(root.join(LAST_REL)).map(|s| s.trim().to_string()).unwrap_or_default()
}

fn write_last(root: &std::path::Path, words: &str) {
    let p = root.join(LAST_REL);
    let _ = p.parent().map(std::fs::create_dir_all);
    if let Err(e) = std::fs::write(&p, words) {
        tracing::warn!(target: "monetize_admin::self_update", "self update: its last word could not be kept at {}: {e}", p.display());
    }
}

/// Where the box keeps the app it last said rolled back (so it is said once per release).
pub const ROLLED_BACK_REL: &str = "solo/self-update.rolled-back";
/// The CODE RED a rolled-back self update raises at monetize.
pub const ROLLED_BACK_RED: &str = "self-update:rolled-back";

/// What the box does about a rollback it sees ([`rolled_back_step`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RolledBackStep {
    /// Say it (ALERT, CODE RED) and keep the app it said.
    Raise(String),
    /// A newer release confirmed (or none is rolled back any more): the CODE RED goes.
    Clear,
    Nothing,
}

/// Pure: **what the box does about a rollback** — from the plan now and the app it last said.
pub fn rolled_back_step(plan: &Plan, next_app: &str, said: &str) -> RolledBackStep {
    match plan {
        Plan::RolledBack(w) if said.trim() != next_app.trim() => RolledBackStep::Raise(w.clone()),
        Plan::RolledBack(_) => RolledBackStep::Nothing,
        _ if !said.trim().is_empty() => RolledBackStep::Clear,
        _ => RolledBackStep::Nothing,
    }
}

/// **PID 1 says a rollback** (owner 2026-10-18): once monetize serves, and every half hour, the
/// box reads its plan; a release it rolled back is said ONCE — `ALERT self-update.rolled-back`
/// (this boot's line: it is mailed) and CODE RED `self-update:rolled-back` at monetize — and
/// cleared when the box runs a newer release or none is rolled back any more.
pub async fn keep_rolled_back(root: std::path::PathBuf) {
    tokio::time::sleep(Duration::from_secs(120)).await;
    loop {
        let r = root.clone();
        if let Ok(Ok((_, n, p))) = tokio::task::spawn_blocking(move || look(&r)).await {
            let said = std::fs::read_to_string(root.join(ROLLED_BACK_REL)).unwrap_or_default();
            match rolled_back_step(&p, &n.app, &said) {
                RolledBackStep::Raise(words) => {
                    tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.rolled-back {words}");
                    let red = monetize_proto::CodeRed {
                        key: ROLLED_BACK_RED.into(),
                        site: "the ONE box".into(),
                        r#box: "this box".into(),
                        what: "a self update was rolled back: the new release is not on this box's record of confirmed releases".into(),
                        error: words,
                        next: "find why the release did not serve (`logs`), publish a newer one; this one is never offered again".into(),
                        ..Default::default()
                    };
                    if code_red(&root, |m| async move { m.estate_orders().raise_code_red(red).await.map(|_| ()) }).await {
                        write_said(&root, &n.app);
                    }
                }
                RolledBackStep::Clear => {
                    let clear = monetize_proto::CodeRedClearRequest { prefix: "self-update:".into() };
                    if code_red(&root, |m| async move { m.estate_orders().clear_code_red(clear).await.map(|_| ()) }).await {
                        let _ = std::fs::remove_file(root.join(ROLLED_BACK_REL));
                    }
                }
                RolledBackStep::Nothing => {}
            }
        }
        tokio::time::sleep(Duration::from_secs(1800)).await;
    }
}

fn write_said(root: &std::path::Path, app: &str) {
    let p = root.join(ROLLED_BACK_REL);
    let _ = p.parent().map(std::fs::create_dir_all);
    let _ = std::fs::write(p, app.trim());
}

/// One call at monetize with its operator bearer; `false` (and said) when it did not go.
async fn code_red<F, Fut>(root: &std::path::Path, call: F) -> bool
where
    F: FnOnce(monetize_ui_server::grpc::Monetize) -> Fut,
    Fut: std::future::Future<Output = Result<(), tonic::Status>>,
{
    let Some(m) = crate::console::operator_client(root, &crate::solo::SERVER.to_string()) else {
        tracing::warn!(target: "monetize_admin::self_update", "self update: no operator bearer: the rollback's CODE RED waits");
        return false;
    };
    match call(m).await {
        Ok(()) => true,
        Err(s) => {
            tracing::warn!(target: "monetize_admin::self_update", "self update: monetize did not take the rollback's CODE RED ({}): asked again", s.message());
            false
        }
    }
}

/// Pure: [`Plan`] as the console reads it — `(plan, why)`.
pub fn plan_words(p: &Plan) -> (&'static str, String) {
    match p {
        Plan::Nothing => ("nothing", String::new()),
        Plan::Live => ("live", String::new()),
        Plan::Base(w) => ("base", w.clone()),
        Plan::Wait(w) => ("wait", w.clone()),
        Plan::RolledBack(w) => ("rolled-back", w.clone()),
        Plan::Config(w) => ("config", w.clone()),
    }
}

/// **PID 1 takes the console's word** (every two seconds): the update done as the door's
/// `POST /self/update` does it, its word kept for the console. A refusal is told to the operator.
pub async fn keep_console_asks(root: std::path::PathBuf) {
    loop {
        tokio::time::sleep(Duration::from_secs(2)).await;
        let Some(by) = take_ask(&root) else { continue };
        tracing::warn!(target: "monetize_admin::self_update", "self update: {by} asked it from the console");
        let r = root.clone();
        let words = match tokio::task::spawn_blocking(move || update(&r)).await {
            Ok(Ok(w)) => w,
            Ok(Err(e)) => {
                tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.refused the update {by} asked was refused: {e}");
                format!("REFUSED {}", e.trim_start_matches("REFUSED "))
            }
            Err(e) => format!("REFUSED [self-update] {e}"),
        };
        write_last(&root, &words);
    }
}

/// OXG-4: **an app's entry asked about `config`** — `entry` read out of the app archive (korp's
/// `.znippy`, each chunk blake3-verified by znippy) into a memfd and run from memory, as
/// korp-start runs it, with `server --check --config <config>`: the start's own checks, starting
/// nothing. `Ok(word)` is the app's own word ([`check_said`]); `Err` says why it could not be
/// asked. The caller verifies the archive first ([`appliance::ask_next_app`]).
#[cfg(target_os = "linux")]
pub fn ask_entry(archive: &[u8], entry: &str, config: &std::path::Path) -> Result<Result<(), String>, String> {
    use std::io::{Read as _, Write as _};
    use std::os::fd::AsRawFd as _;
    let memfd = |name: &std::ffi::CStr, bytes: &[u8]| -> Result<std::fs::File, String> {
        let fd = nix::sys::memfd::memfd_create(name, nix::sys::memfd::MFdFlags::MFD_CLOEXEC).map_err(|e| format!("memfd_create: {e}"))?;
        let mut f = std::fs::File::from(fd);
        f.write_all(bytes).map_err(|e| format!("into its memfd: {e}"))?;
        Ok(f)
    };
    let app = memfd(c"monetize-next-app", archive)?;
    let image = znippy_common::get_file(std::path::Path::new(&format!("/proc/self/fd/{}", app.as_raw_fd())), entry).map_err(|e| format!("`{entry}` out of the archive: {e:#}"))?;
    drop(app);
    let bin = memfd(c"monetize-next-entry", &image)?;
    drop(image);
    // A32-14, a process by design: the NEXT release's own program, asked one question before
    // it is staged (its `server --check`); a box's processes are its design, never cargo or sh.
    let mut child = std::process::Command::new(format!("/proc/self/fd/{}", bin.as_raw_fd()))
        .args(["server", "--check", "--config"])
        .arg(config)
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| format!("the next app does not start: {e}"))?;
    let began = std::time::Instant::now();
    loop {
        match child.try_wait() {
            Ok(Some(_)) => break,
            Ok(None) if began.elapsed() > ASK_DEADLINE => {
                let _ = child.kill();
                let _ = child.wait();
                return Err(format!("its `server --check` did not answer in {} s", ASK_DEADLINE.as_secs()));
            }
            Ok(None) => std::thread::sleep(Duration::from_millis(100)),
            // PID 1's reaper took the exit status first: it ended all the same.
            Err(_) => break,
        }
    }
    drop(bin);
    let (mut out, mut err) = (String::new(), String::new());
    if let Some(mut p) = child.stdout.take() {
        let _ = p.read_to_string(&mut out);
    }
    if let Some(mut p) = child.stderr.take() {
        let _ = p.read_to_string(&mut err);
    }
    Ok(check_said(&out, &err))
}

#[cfg(target_os = "linux")]
mod appliance {
    use super::*;
    use korp_installer::live::{self, Esp};

    fn policy() -> live::Policy {
        let cl = korp_installer::cmdline::effective();
        let signed = korp_installer::cmdline::decide(&cl, std::env::var(korp_installer::cmdline::REQUIRE_SIGNED_ENV).ok().as_deref()).require_signed;
        live::Policy { live_update: cl.live_update_on(), require_signed: signed }
    }

    pub fn running() -> Result<Running, String> {
        let esp = Esp::boot();
        let s = esp.read_setting()?;
        let pin = std::fs::read_to_string(esp.records(s.active).pin).map_err(|e| format!("[live-running] the running slot's pin: {e}"))?;
        let other_app = std::fs::read_to_string(esp.records(s.active.other()).pin).map(|p| p.trim().to_string()).unwrap_or_default();
        Ok(Running { base: live::running_base(&esp)?, app: pin.trim().to_string(), slot: live::render_setting(&s).trim().to_string(), live_update: policy().live_update, other_app, other_confirmed: false })
    }

    /// The other slot's app length and signature (hex), as its korp records hold them.
    pub fn other_records() -> Result<(u64, String), String> {
        let esp = Esp::boot();
        let s = esp.read_setting()?;
        let rec = esp.records(s.active.other());
        let len = std::fs::read_to_string(&rec.len).map_err(|e| format!("[self-back-records] the other slot's length: {e}"))?.trim().parse::<u64>().map_err(|e| format!("[self-back-records] the other slot's length: {e}"))?;
        let sig = std::fs::read(&rec.sig).map(|b| b.iter().map(|x| format!("{x:02x}")).collect::<String>()).unwrap_or_default();
        Ok((len, sig))
    }

    /// **The other slot's own bytes** (OXG-3): the first `len` bytes of its partition, found by
    /// its GPT name as korp-start finds it.
    pub fn read_other_slot(len: u64) -> Result<Vec<u8>, String> {
        use std::io::Read as _;
        if len as usize > MAX_ARCHIVE {
            return Err(format!("its record says {len} bytes, more than the {MAX_ARCHIVE} a slot takes"));
        }
        let slot = Esp::boot().read_setting()?.active.other();
        let part = korp_installer::datahome::find_by_partname(std::path::Path::new("/sys/class/block"), slot.partition()).ok_or_else(|| format!("no `{}` partition on this disk", slot.partition()))?;
        let dev = std::path::Path::new("/dev").join(&part.devname);
        let mut bytes = Vec::with_capacity(len as usize);
        std::fs::File::open(&dev).and_then(|f| f.take(len).read_to_end(&mut bytes)).map_err(|e| format!("{}: {e}", dev.display()))?;
        Ok(bytes)
    }

    /// The other slot's bytes verified as korp-start verifies them at its start ([`live::verify_live`]):
    /// the signature under the ENROLLED root, the rollback floor, the BASE this box runs.
    pub fn verify_other(archive: &[u8], n: &Next) -> Result<(), String> {
        let esp = Esp::boot();
        let pubkey = std::fs::read(esp.root.join("korp/root.pub")).ok().and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
        let floor = korp_installer::rollback::FloorStore::floor(&mut korp_installer::rollback::FloorFile::at(esp.root.join("korp/rollback.floor"))).map_err(|e| format!("[live-floor] {e:#}"))?;
        let running = live::running_base(&esp)?;
        let pin = korp_installer::loader::parse_blake3_hex(&n.app).map_err(|e| format!("[live-pin] {e:#}"))?;
        let sig = unhex(&n.sig).map_err(|e| format!("[self-unsigned] the signature: {e}"))?;
        live::verify_live(&live::Staged { archive, pin, signature: Some(&sig), declared_base: Some(&n.base) }, pubkey.as_ref(), floor, &running, policy()).map(|_| ())
    }

    /// OXG-4: **the next release's app asked about `config`** — its archive verified as the
    /// stage verifies it (the signature under the enrolled root, the floor, the BASE), then its
    /// entry (`korp.entry`, the program korp-start execs) asked ([`super::ask_entry`]).
    pub fn ask_next_app(archive: &[u8], n: &Next, config: &std::path::Path) -> Result<Result<(), String>, String> {
        verify_other(archive, n).map_err(|e| format!("the archive does not verify: {e}"))?;
        let entry = korp_installer::cmdline::effective().entry.unwrap_or_else(|| "korp".into());
        super::ask_entry(archive, &entry, config)
    }

    /// **The way back staged IN PLACE** (OXG-3): the other slot holds the app already (read from
    /// it and proven), so korp's stage verifies it again and writes its records and `<slot>
    /// trial` last — the partition itself is not written. A release declaration the slot
    /// carries is kept with it.
    pub fn stage_other_in_place(archive: &[u8], pin_hex: &str, sig: &[u8], base: &str) -> Result<String, String> {
        let esp = Esp::boot();
        let pubkey = std::fs::read(esp.root.join("korp/root.pub")).ok().and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
        let floor = korp_installer::rollback::FloorStore::floor(&mut korp_installer::rollback::FloorFile::at(esp.root.join("korp/rollback.floor"))).map_err(|e| format!("[live-floor] {e:#}"))?;
        let running = live::running_base(&esp)?;
        let to = esp.read_setting()?.active.other();
        let rec = esp.records(to);
        let bundle = std::fs::read(&rec.release).ok();
        let bundle_sig = std::fs::read(&rec.release_sig).ok();
        let release = bundle.as_deref().map(|b| (b, bundle_sig.as_deref()));
        let p = live::LivePayload { archive, pin_hex, signature: Some(sig), declared_base: Some(base) };
        let mut in_place = |slot: live::Slot, _: &[u8]| if slot == to { Ok(()) } else { Err(format!("[self-back-slot] korp chose slot {slot:?}, the way back is in slot {to:?}")) };
        let slot = live::stage_on_with_release(&esp, &p, release, pubkey.as_ref(), floor, &running, policy(), &mut in_place)?;
        Ok(format!("{slot:?}"))
    }

    pub fn stage(archive: &[u8], pin_hex: &str, sig: &[u8], base: &str) -> Result<String, String> {
        let esp = Esp::boot();
        let pubkey = std::fs::read(esp.root.join("korp/root.pub")).ok().and_then(|b| <[u8; 32]>::try_from(b.as_slice()).ok());
        let floor = korp_installer::rollback::FloorStore::floor(&mut korp_installer::rollback::FloorFile::at(esp.root.join("korp/rollback.floor"))).map_err(|e| format!("[live-floor] {e:#}"))?;
        let running = live::running_base(&esp)?;
        let p = live::LivePayload { archive, pin_hex, signature: Some(sig), declared_base: Some(base) };
        let slot = live::stage_on(&esp, &p, pubkey.as_ref(), floor, &running, policy(), &mut live::write_slot_partition)?;
        Ok(format!("{slot:?}"))
    }

    /// The children (monetize-server, the console) stopped — SIGTERM, then SIGKILL after
    /// [`STOP_GRACE`] — synced, and korp-start (`/init`) exec'd as PID 1. Only returns on failure.
    pub fn stop_children_and_reexec() -> String {
        use nix::sys::signal::{kill, Signal};
        use nix::unistd::Pid;
        // PID 1 is never signalled by kill(-1).
        let _ = kill(Pid::from_raw(-1), Signal::SIGTERM);
        std::thread::sleep(STOP_GRACE);
        let _ = kill(Pid::from_raw(-1), Signal::SIGKILL);
        nix::unistd::sync();
        let argv = [c"/init".to_owned()];
        let envp: Vec<std::ffi::CString> = std::env::vars().filter_map(|(k, v)| std::ffi::CString::new(format!("{k}={v}")).ok()).collect();
        match live::reexec(std::path::Path::new("/init"), &argv, &envp) {
            Ok(never) => match never {},
            Err(e) => e,
        }
    }

    /// **The trial watcher** (PID 1, at every start): on a slot started on trial, confirm once
    /// `serving` has held for [`CONFIRM_AFTER`]; not serving by [`CONFIRM_DEADLINE`], re-exec
    /// korp-start, which rolls the slot back. ★ A32-4: `serving` is the whole word
    /// ([`super::serving_whole`]): its doors answer AND every declared product is served; why
    /// not is said each time it changes.
    pub fn spawn_confirm(serving: impl Fn() -> Result<(), String> + Send + 'static) {
        let esp = Esp::boot();
        let on_trial = esp.read_setting().map(|s| s.trial == live::Trial::Booted).unwrap_or(false);
        if !on_trial {
            return;
        }
        tracing::warn!(target: "monetize_admin::self_update", "self update: this start is a TRIAL of a new release — confirmed after {} s of serving, else rolled back at {} s", CONFIRM_AFTER.as_secs(), CONFIRM_DEADLINE.as_secs());
        // A jera duty (A32-14): a look that panics is said and a FAILED row, and the next runs.
        let began = std::time::Instant::now();
        let mut since: Option<Duration> = None;
        let mut said = String::new();
        let mut first = true;
        let watcher = monetize_server::jobs::every(super::CONFIRM_JOB, Duration::from_secs(5), move || {
            // The first look only waits, as the thread's first sleep did.
            if std::mem::take(&mut first) {
                return Ok(monetize_server::jobs::Round::Again);
            }
            let up = began.elapsed();
            let now = serving();
            if let Err(why) = &now {
                if *why != said {
                    tracing::warn!(target: "monetize_admin::self_update", "self update: the trial does not serve yet: {why}");
                    said = why.clone();
                }
            }
            since = if now.is_ok() { since.or(Some(up)) } else { None };
            match confirm_plan(true, up, since) {
                ConfirmStep::Confirm => {
                    match live::confirm_on(&esp) {
                        Ok(s) => {
                            tracing::warn!(target: "monetize_admin::self_update", "self update: the new release serves — slot confirmed ({})", live::render_setting(&s).trim());
                            // R34-4: on the record of apps this box ran confirmed, at once.
                            // M-FOLLOW 3: and the app this trial came from (it ran confirmed).
                            if let Ok(mut r) = running() {
                                confirmed_from(&crate::data_root(), &r);
                                known_confirmed(&crate::data_root(), &mut r);
                            }
                        }
                        Err(e) => tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.confirm the new release serves but its slot could not be confirmed: {e}"),
                    }
                    return Ok(monetize_server::jobs::Round::Done);
                }
                ConfirmStep::RollBack => {
                    tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.rollback the new release did not serve within {} s — rolled back by korp-start", CONFIRM_DEADLINE.as_secs());
                    // The alert outlives the re-exec: the waiting lines to disk first.
                    crate::alerts::spool_now(&crate::data_root());
                    let e = stop_children_and_reexec();
                    tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.reexec the rollback re-exec FAILED (the children are stopped): {e}");
                    return Ok(monetize_server::jobs::Round::Done);
                }
                ConfirmStep::Wait | ConfirmStep::Nothing => {}
            }
            Ok(monetize_server::jobs::Round::Again)
        });
        if let Err(e) = watcher {
            tracing::error!(target: "monetize_admin::self_update", "ALERT self-update.confirm no trial watcher ({e}): this trial is never confirmed, and the next start rolls it back");
        }
    }
}

#[cfg(not(target_os = "linux"))]
mod appliance {
    use super::*;
    pub fn running() -> Result<Running, String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn stage(_: &[u8], _: &str, _: &[u8], _: &str) -> Result<String, String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn other_records() -> Result<(u64, String), String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn read_other_slot(_: u64) -> Result<Vec<u8>, String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn verify_other(_: &[u8], _: &Next) -> Result<(), String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn ask_next_app(_: &[u8], _: &Next, _: &std::path::Path) -> Result<Result<(), String>, String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn stage_other_in_place(_: &[u8], _: &str, _: &[u8], _: &str) -> Result<String, String> {
        Err("[live-unsupported] not a korp box".into())
    }
    pub fn stop_children_and_reexec() -> String {
        "[live-unsupported] not a korp box".into()
    }
    pub fn spawn_confirm(_: impl Fn() -> Result<(), String> + Send + 'static) {}
}

pub use appliance::spawn_confirm;

/// ★ **A32-4: the trial serves WHOLE** — monetize-server answers on its loopback port AND its
/// word on what it serves ([`monetize_server::serving`], `<data_dir>/serving.json`), written by
/// this start's app, says every product its feed declares is served. A bare TCP connect
/// confirmed an app that refused every product guest (R3+R4). `since_ms`: this PID 1's start.
pub fn serving_whole(root: &std::path::Path, since_ms: u64) -> Result<(), String> {
    std::net::TcpStream::connect_timeout(&std::net::SocketAddr::V4(crate::solo::SERVER), Duration::from_secs(2)).map_err(|e| format!("monetize does not answer on {}: {e}", crate::solo::SERVER))?;
    let path = monetize_server::config::Config { data_dir: root.join(monetize_front::intake::DATA_REL), ..Default::default() }.serving_path();
    let word = std::fs::read_to_string(&path).ok().and_then(|t| serde_json::from_str::<monetize_server::serving::Serving>(&t).ok());
    serves_whole(word.as_ref(), since_ms)
}

/// Pure: **does monetize's word `s` say it serves whole**, written since `since_ms`? `Err`: why
/// not, by name (the product not served and its refusal).
pub fn serves_whole(s: Option<&monetize_server::serving::Serving>, since_ms: u64) -> Result<(), String> {
    let Some(s) = s else { return Err("monetize has not said what it serves yet".into()) };
    if s.at_unix_ms < since_ms {
        return Err(format!("monetize's word on what it serves is from before this start ({} < {since_ms}): the new app has not said it yet", s.at_unix_ms));
    }
    if !s.refused.is_empty() {
        return Err(format!("not every product is served: {}", s.refused.join("; ")));
    }
    let missing: Vec<&str> = s.declared.iter().filter(|d| !s.served.contains(d)).map(String::as_str).collect();
    if !missing.is_empty() {
        return Err(format!("not every product is served: {} not served", missing.join(", ")));
    }
    Ok(())
}

/// The jera duty that watches a new release's trial (A32-14).
pub const CONFIRM_JOB: &str = "monetize_self_update_confirm";

/// **A named thread for PID 1's own short work**, or why the box could not start one: a
/// thread that cannot start is an answer, never a panic in PID 1 (raw `thread::spawn` panics).
fn named(name: &str, f: impl FnOnce() + Send + 'static) -> Result<(), String> {
    // A32-14, stays a thread: its one user is the re-exec after the answer leaves, which IS PID 1's
    // stop (execve ends it); every other background work of PID 1 is a jera duty.
    std::thread::Builder::new().name(name.into()).spawn(f).map(|_| ()).map_err(|e| format!("the {name} thread could not start: {e}"))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// ★ **A32-4: the trial confirms only when the new app SERVES** — its doors answer AND every
    /// product its feed declares is served (its guest loaded), as this boot's monetize wrote it
    /// (`monetize_server::serving`). A bare TCP connect confirmed an app that refused every
    /// product guest (R3+R4, release-r34.md 03:14:52): that is now a wait, said by name.
    #[test]
    fn the_trial_confirms_only_when_every_declared_product_is_served_by_this_boot() {
        use monetize_server::serving::Serving;
        let boot = 1_900_000_000_000;
        let all = Serving { pid: 7, at_unix_ms: boot + 5_000, declared: vec!["gunnar".into(), "holger".into()], served: vec!["gunnar".into(), "holger".into()], refused: Vec::new() };
        assert_eq!(serves_whole(Some(&all), boot), Ok(()));
        let gap = Serving { served: vec!["gunnar".into()], refused: vec!["holger: guest-refused-at-start (built for another contract)".into()], ..all.clone() };
        let why = serves_whole(Some(&gap), boot).unwrap_err();
        assert!(why.contains("holger") && why.contains("guest-refused-at-start"), "the product not served is named: {why}");
        let stale = Serving { at_unix_ms: boot - 1, ..all.clone() };
        assert!(serves_whole(Some(&stale), boot).unwrap_err().contains("before this start"), "the old app's word is not this app's");
        assert!(serves_whole(None, boot).unwrap_err().contains("not said"), "no word yet: wait");
        let none_declared = Serving { declared: Vec::new(), served: Vec::new(), ..all };
        assert_eq!(serves_whole(Some(&none_declared), boot), Ok(()), "a box that sells nothing serves its doors");
    }

    /// ★ **PID 1's threads carry their name** (S2, score robustness.threads): the re-exec and the
    /// trial watcher run on a named thread, so a stack dump or `top -H` says which is which.
    #[test]
    fn a_named_thread_runs_under_its_name() {
        let (tx, rx) = std::sync::mpsc::channel();
        named("self-update-test", move || {
            let _ = tx.send(std::thread::current().name().map(str::to_string));
        })
        .expect("a thread starts");
        assert_eq!(rx.recv_timeout(Duration::from_secs(5)).unwrap().as_deref(), Some("self-update-test"));
    }

    fn next() -> Next {
        let a = archive();
        let sha = ring::digest::digest(&ring::digest::SHA256, &a);
        Next {
            base: "b".repeat(64),
            app: blake3::hash(&a).to_hex().to_string(),
            app_len: a.len() as u64,
            sig: "ab".repeat(80),
            object: "release/monetize-admin-live/x.live-app.znippy".into(),
            sha256: sha.as_ref().iter().map(|b| format!("{b:02x}")).collect(),
            build: "6260c99".into(),
        }
    }

    fn archive() -> Vec<u8> {
        (0..8192u32).map(|i| (i % 253) as u8).collect()
    }

    fn running(base: &str, app: &str, slot: &str) -> Running {
        Running { base: base.into(), app: app.into(), slot: slot.into(), live_update: true, other_app: String::new(), other_confirmed: false }
    }

    /// ★ **OXG-4 (REL-T2ONE, test2 ONE box 2026-10-29): `box update` refuses, before anything is
    /// staged, a release whose app cannot read the box's fed config.** The app 40c5e923 was
    /// staged on trial and answered `[self-no-config] … plans ([products.<id>.plans]) is retired
    /// (RA2-16)`: the box's feed was older than the release. It never confirmed, korp-start
    /// rolled it back, and the box lost its way back. The plan now asks the next app's own parse
    /// first (here: this build's, the app of the release) and says what to feed first; a config
    /// it reads stays `Live`, and a next feed staged for that app is the config it starts on.
    #[test]
    fn the_plan_refuses_a_release_whose_app_cannot_read_the_boxs_fed_config() {
        let d = tempfile::tempdir().unwrap();
        let at = |name: &str, text: &str| {
            let p = d.path().join(name);
            std::fs::write(&p, format!("data_dir = {:?}\n{text}", d.path().join("data").display().to_string())).unwrap();
            p
        };
        let old_shape = at("old.toml", "[products.x]\ninstance = \"x\"\n[products.x.plans.\"p\"]\ncaps = { disk_bytes = 5 }\nlabel = \"P\"\n");
        let new_shape = at("new.toml", "[products.x]\ninstance = \"x\"\n");
        // The release's app parse: monetize-server's own load, as its `server --check` runs it.
        let parse = |p: &std::path::Path| monetize_server::config::Config::load(p).map(|_| ()).map_err(|e| format!("{e:#}"));
        let n = next();
        let p = plan_with_config(Plan::Live, &n, false, || parse(&old_shape));
        let Plan::Config(w) = &p else { panic!("an old-shape config must refuse the plan: {p:?}") };
        assert!(w.contains("[self-next-config]") && w.contains(&n.build[..7]), "refused by name, the release named: {w}");
        assert!(w.contains("plans"), "the app's own reason: {w}");
        assert!(w.contains("feed --apply") && w.contains("feed --next") && w.contains("nothing was staged"), "what to feed first: {w}");
        assert_eq!(plan_words(&p).0, "config");
        assert!(update_words(&p).unwrap_err().starts_with("REFUSED [self-next-config]"), "the apply refuses as the plan does");
        assert_eq!(next_feed_for(&running(&n.base, &"0".repeat(64), "a"), &n, &p), Ok(n.app.clone()), "staging the next release's feed is one way to feed first");
        assert_eq!(plan_with_config(Plan::Live, &n, false, || parse(&new_shape)), Plan::Live, "a config the app reads: live");
        assert_eq!(plan_with_config(Plan::Live, &n, true, || panic!("not asked")), Plan::Live, "a next feed staged for that app is the config it starts on");
        let wait = Plan::Wait("[live-trial-open] slot b trial".into());
        assert_eq!(plan_with_config(wait.clone(), &n, false, || panic!("not asked")), wait, "only a live plan asks");
    }

    /// ★ OXG-4: **the next app's word** is its `server --check` output: `ok: …` on stdout, else
    /// the reason on stderr (its first line), never silence taken for yes.
    #[test]
    fn the_next_apps_check_word_is_ok_or_its_reason() {
        assert_eq!(check_said("start would say: x\nok: /data/monetize/monetize.toml is a config this server starts on\n", ""), Ok(()));
        let no = check_said("", "monetize-admin server: parse /data/monetize/monetize.toml line 785: `plans` ([products.<id>.plans.<plan>]) is retired (RA2-16)\n").unwrap_err();
        assert!(no.contains("line 785") && no.contains("retired") && !no.starts_with("monetize-admin server:"), "{no}");
        assert!(check_said("", "").unwrap_err().contains("said nothing"));
    }

    /// A static x86-64 ELF (no libc, no interpreter) that writes `msg` to `fd` and exits `code`.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn says_elf(fd: u8, msg: &[u8], code: u8) -> Vec<u8> {
        const BASE: u64 = 0x40_0000;
        let at = 64 + 56;
        let len = (msg.len() as u32).to_le_bytes();
        // write(fd, msg, len); exit(code) — the message right after the 36 bytes of code.
        let mut text = vec![0xb8, 1, 0, 0, 0, 0xbf, fd, 0, 0, 0, 0x48, 0x8d, 0x35, 19, 0, 0, 0, 0xba, len[0], len[1], len[2], len[3], 0x0f, 0x05, 0xb8, 60, 0, 0, 0, 0xbf, code, 0, 0, 0, 0x0f, 0x05];
        text.extend_from_slice(msg);
        let size = (at + text.len()) as u64;
        let mut e = vec![0x7f, b'E', b'L', b'F', 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
        e.extend_from_slice(&2u16.to_le_bytes());
        e.extend_from_slice(&0x3eu16.to_le_bytes());
        e.extend_from_slice(&1u32.to_le_bytes());
        e.extend_from_slice(&(BASE + at as u64).to_le_bytes());
        e.extend_from_slice(&64u64.to_le_bytes());
        e.extend_from_slice(&0u64.to_le_bytes());
        e.extend_from_slice(&0u32.to_le_bytes());
        for h in [64u16, 56, 1, 0, 0, 0] {
            e.extend_from_slice(&h.to_le_bytes());
        }
        e.extend_from_slice(&1u32.to_le_bytes());
        e.extend_from_slice(&5u32.to_le_bytes());
        for w in [0u64, BASE, BASE, size, size, 0x1000] {
            e.extend_from_slice(&w.to_le_bytes());
        }
        e.extend_from_slice(&text);
        e
    }

    /// ★ OXG-4: **the next app is asked from its own archive**: the entry read out of korp's
    /// `.znippy` into memory and run as `server --check --config <the box's config>`; its `ok:`
    /// is yes, its reason on stderr is the refusal, and an entry the archive lacks is "could not
    /// be asked" (the plan stands), never a refusal.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    fn the_next_app_is_asked_out_of_its_own_archive() {
        let cfg = std::path::Path::new("/data/monetize/monetize.toml");
        let pack = |elf: Vec<u8>| znippy_common::create_archive_to_vec(&[("app".to_string(), elf)], 3).expect("an app archive").0;
        let yes = pack(says_elf(1, b"ok: /data/monetize/monetize.toml is a config this server starts on\n", 0));
        assert_eq!(ask_entry(&yes, "app", cfg), Ok(Ok(())));
        let no = pack(says_elf(2, b"monetize-admin server: parse line 785: `plans` ([products.<id>.plans.<plan>]) is retired (RA2-16)\n", 1));
        let word = ask_entry(&no, "app", cfg).expect("asked").unwrap_err();
        assert!(word.contains("retired") && word.contains("line 785"), "{word}");
        assert!(ask_entry(&yes, "korp", cfg).unwrap_err().contains("`korp` out of the archive"), "an entry it lacks cannot be asked");
    }

    /// ★ **A release the box rolled back is never offered again** (owner 2026-10-18, the mock's
    /// broken release rolled back at 655 s and was still offered `Live`): the other slot holds
    /// the next release's app, the box runs another, no trial open — it is said rolled back, by
    /// its build; a NEWER release (another app) is offered live as ever.
    #[test]
    fn a_release_the_box_rolled_back_is_never_offered_again() {
        let n = next();
        let back = Running { other_app: n.app.clone(), ..running(&"b".repeat(64), &"0".repeat(64), "b") };
        assert!(matches!(decide(&back, &n), Plan::RolledBack(w) if w.contains("rolled back") && w.contains(&n.build[..7]) && w.contains("not offered again")), "{:?}", decide(&back, &n));
        let newer = Next { app: "9".repeat(64), ..n.clone() };
        assert_eq!(decide(&back, &newer), Plan::Live, "a newer release is offered");
        let after_a_good_update = Running { other_app: "0".repeat(64), ..running(&"b".repeat(64), &n.app, "a") };
        assert_eq!(decide(&after_a_good_update, &n), Plan::Nothing, "the slot left behind by a confirmed update is the OLD app: nothing rolled back");
        assert!(matches!(decide(&Running { slot: "a booted".into(), ..back.clone() }, &n), Plan::Wait(_)), "a trial still open is not a rollback yet");
        assert_eq!(plan_words(&decide(&back, &n)).0, "rolled-back");
    }

    /// ★ **R34-4: the release in the other slot that CONFIRMED here is the operator's way back**
    /// (rehearsal 2026-10-25: after a confirmed update the previous release, confirmed on the
    /// box at 11:16Z, sat in the other slot; `box running` said "rolled back … it never
    /// confirmed on this box" and `box update` refused it, while a release two back was
    /// offered). The box keeps a record of every app it ran confirmed; the other slot's app on
    /// that record is offered live like any release; one NOT on it is still never offered.
    #[test]
    fn the_other_slots_release_that_confirmed_here_is_offered_back() {
        let d = tempfile::tempdir().unwrap();
        let n = next();
        let old = "0".repeat(64);
        // The box ran `old` confirmed (slot a, no trial), then took `n` live and confirmed it.
        let mut before = running(&"b".repeat(64), &old, "a");
        known_confirmed(d.path(), &mut before);
        let mut on_trial = Running { other_app: old.clone(), ..running(&"b".repeat(64), &n.app, "b booted") };
        known_confirmed(d.path(), &mut on_trial);
        assert!(on_trial.other_confirmed, "the slot it came from confirmed here");
        let mut after = Running { other_app: old.clone(), ..running(&"b".repeat(64), &n.app, "b") };
        known_confirmed(d.path(), &mut after);
        // The operator puts the old release's record back and asks: it is offered.
        let back = Next { app: old.clone(), ..n.clone() };
        assert_eq!(decide(&after, &back), Plan::Live, "the previous release, confirmed here, is the way back");
        // A release korp-start rolled back never ran confirmed: still never offered again.
        let mut rolled = Running { other_app: "9".repeat(64), ..running(&"b".repeat(64), &n.app, "b") };
        known_confirmed(d.path(), &mut rolled);
        assert!(!rolled.other_confirmed);
        assert!(matches!(decide(&rolled, &Next { app: "9".repeat(64), ..n.clone() }), Plan::RolledBack(_)));
        // A trial still open is never recorded as confirmed.
        let mut t = running(&"b".repeat(64), &"7".repeat(64), "a booted");
        known_confirmed(d.path(), &mut t);
        let mut later = Running { other_app: "7".repeat(64), ..running(&"b".repeat(64), &n.app, "b") };
        known_confirmed(d.path(), &mut later);
        assert!(!later.other_confirmed, "an app seen only on trial never confirmed");
    }

    /// ★ The operator's word stages live only on the same BASE, signed, live on, no trial open;
    /// the box on the next app already does nothing; another BASE is the kit's keep upgrade.
    #[test]
    fn the_operators_word_stages_live_only_on_the_same_base_and_signed() {
        let n = next();
        assert_eq!(decide(&running(&"b".repeat(64), &"0".repeat(64), "a"), &n), Plan::Live);
        assert_eq!(decide(&running(&"b".repeat(64), &n.app, "b"), &n), Plan::Nothing);
        assert!(matches!(decide(&running(&"c".repeat(64), &"0".repeat(64), "a"), &n), Plan::Base(w) if w.contains("cargo xtask upgrade")));
        assert!(matches!(decide(&Running { live_update: false, ..running(&"b".repeat(64), &"0".repeat(64), "a") }, &n), Plan::Base(w) if w.contains("OFF")));
        assert!(matches!(decide(&running(&"b".repeat(64), &"0".repeat(64), "b booted"), &n), Plan::Wait(_)));
        assert!(matches!(decide(&running(&"b".repeat(64), &"0".repeat(64), "a"), &Next { sig: String::new(), ..n }), Plan::Base(w) if w.contains("unsigned")));
    }

    /// ★ The record names its archive in the release bucket; the archive must be what it says.
    #[test]
    fn the_record_and_the_archive_are_proven() {
        let n = next();
        let text = format!("base = \"{}\"\napp = \"{}\"\napp_len = {}\nsig = \"{}\"\nobject = \"{}\"\nsha256 = \"{}\"\nbuild = \"{}\"\n", n.base, n.app, n.app_len, n.sig, n.object, n.sha256, n.build);
        assert_eq!(parse_next(&text).unwrap(), n);
        assert!(parse_next(&text.replace("release/monetize-admin-live", "../x")).unwrap_err().contains("not a release object"));
        assert!(parse_next("base = \"x\"\n").unwrap_err().contains("[self-next]"));
        assert!(prove(&archive(), &n).is_ok());
        let mut other = archive();
        other[0] ^= 1;
        assert!(prove(&other, &n).unwrap_err().contains("sha256"));
        assert!(prove(&archive()[..10], &n).unwrap_err().contains("bytes"));
        assert_eq!(bucket_base("https://objsto.example/gunnar-release/release/gunnar/x.iso?X-Amz=1").as_deref(), Some("https://objsto.example/gunnar-release"));
        assert_eq!(bucket_base("http://objsto.example/release/front/a.iso"), None, "never a plain-http bucket");
        assert_eq!(bucket_base("http://127.0.0.1:17463/objsto/release/release/front/a.iso").as_deref(), Some("http://127.0.0.1:17463/objsto/release"), "the bench's mock bucket");
        assert_eq!(reachable("http://127.0.0.1:17463/objsto/release/x"), "http://10.0.2.100:17463/objsto/release/x");
        assert_eq!(reachable("https://objsto.example/x"), "https://objsto.example/x");
    }

    /// ★ The new release confirms after 60 s of serving; one that never serves rolls back at 600 s.
    #[test]
    fn a_trial_confirms_after_serving_and_rolls_back_when_it_never_serves() {
        assert_eq!(confirm_plan(false, Duration::from_secs(900), None), ConfirmStep::Nothing);
        assert_eq!(confirm_plan(true, Duration::from_secs(70), Some(Duration::from_secs(5))), ConfirmStep::Confirm);
        assert_eq!(confirm_plan(true, Duration::from_secs(30), Some(Duration::from_secs(5))), ConfirmStep::Wait);
        assert_eq!(confirm_plan(true, Duration::from_secs(601), None), ConfirmStep::RollBack);
    }

    /// ★ **"Update this box" (owner 2026-10-18)**: the console's word waits for PID 1 once — a
    /// second ask while it waits is refused — and is gone once taken; the plan reads in words.
    #[test]
    fn the_consoles_word_waits_once_and_is_taken_once() {
        let root = std::env::temp_dir().join(format!("self-ask-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        assert!(!asked(&root) && take_ask(&root).is_none());
        ask(&root, "console:alice").expect("asked");
        assert!(asked(&root));
        assert!(ask(&root, "console:bob").unwrap_err().contains("asked already"));
        assert_eq!(take_ask(&root).as_deref(), Some("console:alice"));
        assert!(!asked(&root) && take_ask(&root).is_none(), "taken once");
        write_last(&root, "staged release fd0ed14");
        assert_eq!(last(&root), "staged release fd0ed14");
        assert_eq!(plan_words(&Plan::Live), ("live", String::new()));
        assert_eq!(plan_words(&Plan::Base("off".into())), ("base", "off".into()));
        let _ = std::fs::remove_dir_all(&root);
    }

    /// ★ **A rollback is said once per release and cleared when it is over** (owner 2026-10-18).
    #[test]
    fn a_rollback_is_said_once_and_cleared_when_over() {
        let back = Plan::RolledBack("rolled back: 1cbf59e — not offered again".into());
        assert_eq!(rolled_back_step(&back, "aa", ""), RolledBackStep::Raise("rolled back: 1cbf59e — not offered again".into()));
        assert_eq!(rolled_back_step(&back, "aa", "aa"), RolledBackStep::Nothing, "said once");
        assert_eq!(rolled_back_step(&Plan::Nothing, "bb", "aa"), RolledBackStep::Clear, "a newer release runs: over");
        assert_eq!(rolled_back_step(&Plan::Live, "bb", ""), RolledBackStep::Nothing);
    }

    /// ★ **M-FOLLOW 3 (R34-4, prod 2026-10-26): the operator's way back is the other slot's
    /// release that confirmed here, taken by the box from its OWN slot records** — never a
    /// pointer put back in the shared bucket. Refused by name while a trial is open (korp-start's
    /// own rollback owns the confirm window), when the other slot holds nothing, and when its
    /// app is not on the record of confirmed apps (korp-start rolled it back).
    #[test]
    fn the_way_back_is_the_other_slots_confirmed_release_from_the_boxs_own_records() {
        let base = "b".repeat(64);
        let (old, new) = ("0".repeat(64), "1".repeat(64));
        let after = Running { other_app: old.clone(), other_confirmed: true, ..running(&base, &new, "b") };
        let back = decide_back(&after, 8192, &"ab".repeat(80)).expect("the way back");
        assert_eq!((back.app.as_str(), back.app_len, back.base.as_str()), (old.as_str(), 8192, base.as_str()));
        assert!(back.object.is_empty(), "the way back names no bucket object (OXG-3): {}", back.object);
        assert!(back_words(&after, &Ok::<(), String>(())).contains("box update --back") && back_words(&after, &Ok::<(), String>(())).contains(&old[..12]), "{}", back_words(&after, &Ok::<(), String>(())));
        // The confirm window is korp-start's: no operator rollback while a trial is open.
        let trial = Running { slot: "b booted".into(), ..after.clone() };
        assert!(decide_back(&trial, 8192, "ab").unwrap_err().contains("[live-trial-open]"));
        // A release korp-start rolled back never confirmed here: never taken back.
        let rolled = Running { other_confirmed: false, ..after.clone() };
        let no = decide_back(&rolled, 8192, "ab").unwrap_err();
        assert!(no.contains("[self-back-unconfirmed]") && no.contains("record of confirmed"), "{no}");
        assert!(!no.contains("never confirmed"), "the box says what it knows, never more: {no}");
        assert!(back_words(&rolled, &Ok::<(), String>(())).starts_with("no way back"), "{}", back_words(&rolled, &Ok::<(), String>(())));
        let none = Running { other_app: String::new(), ..after.clone() };
        assert!(decide_back(&none, 0, "").unwrap_err().contains("[self-back-none]"));
        assert!(decide_back(&Running { live_update: false, ..after.clone() }, 8192, "ab").unwrap_err().contains("OFF"));
        assert!(decide_back(&after, 8192, "").unwrap_err().contains("[self-unsigned]"));
        // The archive proven by its length and pin (a slot record has no sha256).
        let a = archive();
        let pin = blake3::hash(&a).to_hex().to_string();
        let back = decide_back(&Running { other_app: pin, ..after.clone() }, a.len() as u64, "ab").unwrap();
        assert_eq!(prove(&a, &back), Ok(()));
        assert!(prove(&a[1..], &back).is_err());
    }

    /// ★ **OXG-3 (ONE box KVM bench 2026-10-29): the way back is taken from the box's OWN other
    /// slot, never from the bucket.** Install at A, self-release + update to B, confirmed: `box
    /// running` offered `--back`, and the apply answered `[self-fetch] release/monetize-admin-live/
    /// <A>.live-app.znippy: 404` — an app laid by install or upgrade is never published in the
    /// bucket. The other slot still holds A: its bytes, proven by its records' length and pin, are
    /// the way back. A slot that cannot be read or does not hold its app whole refuses by name,
    /// and `box running` offers `--back` only when it can be taken.
    #[test]
    fn the_way_back_is_taken_from_the_boxs_own_other_slot_never_the_bucket() {
        let a = archive();
        let pin = blake3::hash(&a).to_hex().to_string();
        let after = Running { other_app: pin.clone(), other_confirmed: true, ..running(&"b".repeat(64), &"1".repeat(64), "b") };
        let sig = "ab".repeat(80);
        // The bench's case: the bucket holds no archive of A at all — the slot does.
        let (n, bytes) = way_back(&after, a.len() as u64, &sig, |len| Ok(a[..len as usize].to_vec())).expect("the way back from the slot itself");
        assert_eq!((n.app.as_str(), n.app_len, n.sig.as_str(), n.base.as_str()), (pin.as_str(), a.len() as u64, sig.as_str(), "b".repeat(64).as_str()));
        assert_eq!(bytes, a, "the slot's own bytes are staged");
        assert!(n.object.is_empty(), "no bucket object is named: {}", n.object);
        let ok = way_back(&after, a.len() as u64, &sig, |len| Ok(a[..len as usize].to_vec()));
        assert!(back_words(&after, &ok).contains("box update --back --apply"), "{}", back_words(&after, &ok));
        // A slot that cannot be read: refused by name, and not offered.
        let unread = way_back(&after, a.len() as u64, &sig, |_| Err("no `korp-rootfs` partition on this disk".into()));
        let e = unread.as_ref().map(|_| ()).unwrap_err();
        assert!(e.contains("[self-back-slot]") && e.contains("korp-rootfs"), "{e}");
        assert!(back_words(&after, &unread).starts_with("no way back") && back_words(&after, &unread).contains("[self-back-slot]"), "{}", back_words(&after, &unread));
        // A slot that does not hold its app whole (overwritten, short): refused by name.
        let mut other = a.clone();
        other[7] ^= 1;
        let e = way_back(&after, a.len() as u64, &sig, |_| Ok(other.clone())).map(|_| ()).unwrap_err();
        assert!(e.contains("[self-back-slot]") && e.contains("pin"), "{e}");
        let e = way_back(&after, a.len() as u64, &sig, |_| Ok(a[1..].to_vec())).map(|_| ()).unwrap_err();
        assert!(e.contains("[self-back-slot]") && e.contains("bytes"), "{e}");
        // The confirm window stays korp-start's: refused, and the slot is not even read.
        let trial = Running { slot: "b booted".into(), ..after.clone() };
        let e = way_back(&trial, a.len() as u64, &sig, |_| panic!("the slot is not read inside the confirm window")).map(|_| ()).unwrap_err();
        assert!(e.contains("[live-trial-open]"), "{e}");
        // An empty other slot: no way back, said by name.
        let none = Running { other_app: String::new(), ..after.clone() };
        assert!(way_back(&none, 0, "", |_| panic!("nothing to read")).map(|_| ()).unwrap_err().contains("[self-back-none]"));
    }

    /// ★ **M-FOLLOW 3: at the confirm, the app the trial came from is on the record** — it ran
    /// confirmed when the update was taken (an update is refused while a trial is open). That
    /// heals a box updated by an OLDER app that kept no record (prod: e9fc1d56 in slot b). An
    /// app korp-start rolled back is never recorded: the old app runs again and the failed one
    /// never confirmed.
    #[test]
    fn at_the_confirm_the_app_the_trial_came_from_is_recorded() {
        let d = tempfile::tempdir().unwrap();
        let base = "b".repeat(64);
        let (old, new) = ("0".repeat(64), "1".repeat(64));
        // The older app kept no record; the new one confirms in slot a.
        let mut confirmed = Running { other_app: old.clone(), ..running(&base, &new, "a") };
        confirmed_from(d.path(), &confirmed);
        known_confirmed(d.path(), &mut confirmed);
        assert!(confirmed.other_confirmed, "the app it came from ran confirmed");
        assert!(decide_back(&confirmed, 1, "ab").is_ok());
        // korp-start rolled a failed trial back: the old app runs, the failed one is not recorded.
        let failed = "2".repeat(64);
        let mut rolled = Running { other_app: failed.clone(), ..running(&base, &new, "a") };
        known_confirmed(d.path(), &mut rolled);
        assert!(!rolled.other_confirmed);
        assert!(matches!(decide(&rolled, &Next { app: failed, ..next() }), Plan::RolledBack(w) if !w.contains("never confirmed")));
    }

    /// The door's ask: an empty body is the next release; `{"back": true}` the way back.
    #[test]
    fn the_doors_ask_is_the_next_release_or_the_way_back() {
        assert_eq!(ask_of(""), Ok(false));
        assert_eq!(ask_of("{}"), Ok(false));
        assert_eq!(ask_of(r#"{"back": true}"#), Ok(true));
        assert_eq!(ask_of(r#"{"back": false}"#), Ok(false));
        assert!(ask_of(r#"{"back": "yes"}"#).unwrap_err().contains("[self-ask]"));
    }

    /// ★ **M-FOLLOW 4 (R34-15): each box reads its OWN pointer** — prod and test2 shared
    /// `release/monetize-admin/next.live.toml`, so a boot check on test2 moved what prod was
    /// offered. The box's own key is named by its host (the fed `web_hosts`, the kit's
    /// `[boxes.<b>] host`); only when the bucket holds none (404: a kit that has not written
    /// it yet) is the shared one read, so today's boxes and kits keep working. An error that is
    /// not a 404 is said, never papered over with the shared pointer.
    #[test]
    fn each_box_reads_its_own_pointer_and_the_shared_one_only_when_it_has_none() {
        assert_eq!(next_key_of("test2.monetize.ignalina.dk").as_deref(), Some("release/monetize-admin/boxes/test2.monetize.ignalina.dk/next.live.toml"));
        assert_eq!(next_key_of(" Monetize.Ignalina.DK ").as_deref(), Some("release/monetize-admin/boxes/monetize.ignalina.dk/next.live.toml"));
        for bad in ["", "a/b", "..", "x..y", "-x", "a b"] {
            assert_eq!(next_key_of(bad), None, "{bad:?}");
        }
        let own = next_key_of("test2.monetize.ignalina.dk").unwrap();
        let bucket = |have: &'static [&'static str]| move |k: &str| -> Result<Option<Vec<u8>>, String> { Ok(have.contains(&k).then(|| k.as_bytes().to_vec())) };
        // Its own key, when the kit wrote it — the shared one is never read then.
        let both: &'static [&'static str] = Box::leak(vec![Box::leak(own.clone().into_boxed_str()) as &str, NEXT_KEY].into_boxed_slice());
        assert_eq!(read_next("test2.monetize.ignalina.dk", bucket(both)).unwrap().0, own);
        // The migration: no own key yet → the shared one, as today.
        assert_eq!(read_next("test2.monetize.ignalina.dk", bucket(&[NEXT_KEY])).unwrap().0, NEXT_KEY);
        // A box with no host to name it reads the shared one.
        assert_eq!(read_next("", bucket(both)).unwrap().0, NEXT_KEY);
        // A bucket that answers an error for the own key is an error, not the shared pointer.
        let down = |k: &str| -> Result<Option<Vec<u8>>, String> { if k == NEXT_KEY { Ok(Some(b"shared".to_vec())) } else { Err("[self-fetch] the bucket answered 503".into()) } };
        assert!(read_next("test2.monetize.ignalina.dk", down).unwrap_err().contains("503"));
        assert!(read_next("test2.monetize.ignalina.dk", bucket(&[])).unwrap_err().contains("[self-next]"));
    }

    /// ★ M-FOLLOW 7: a next feed is staged only for a release this box would take live: the one
    /// its own pointer names, not the app it runs, not one rolled back or refused.
    #[test]
    fn a_next_feed_is_staged_only_for_the_release_this_box_takes_live() {
        let n = next();
        let r = running(&"b".repeat(64), &"0".repeat(64), "a");
        assert_eq!(next_feed_for(&r, &n, &decide(&r, &n)), Ok(n.app.clone()));
        let runs = running(&"b".repeat(64), &n.app, "a");
        assert!(next_feed_for(&runs, &n, &decide(&runs, &n)).unwrap_err().contains("[feed-next-runs]"));
        let trial = running(&"b".repeat(64), &"0".repeat(64), "a booted");
        assert!(next_feed_for(&trial, &n, &decide(&trial, &n)).unwrap_err().contains("[live-trial-open]"));
        let back = Running { other_app: n.app.clone(), ..r.clone() };
        assert!(next_feed_for(&back, &n, &decide(&back, &n)).unwrap_err().contains("rolled back"));
        let base = running(&"c".repeat(64), &"0".repeat(64), "a");
        assert!(next_feed_for(&base, &n, &decide(&base, &n)).unwrap_err().contains("keep upgrade"));
    }
}