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

nordisk/monetize

crates/monetize-cloud-wasm/src/lib.rs

//! **The cloud guest host** — a signed `.wasm` behind the same
//! [`monetize_cloud::factory::CloudFactory`] a linked cloud registers through.
//!
//! ```text
//!  [clouds.<name>] guest = "<name>.wasm"   ──▶ factory(name).build(&CloudBuild)
//!       guest_digest / guest_trust_roots   ──▶ refuse by name unless the bytes are vouched for
//!       grants = ["http", "state"]         ──▶ the linker defines exactly those doors
//!       http_hosts = ["api.upcloud.com"]   ──▶ the only hosts the HTTP door reaches
//!       every other key                    ──▶ the guest's own settings, sent with each call
//!                                              ▼
//!                           GuestCloud: Box<dyn Cloud>
//!                           every verb = one plugin_call(op, JSON) under fuel + a deadline
//!                                              ▼
//!                           the server wraps it in SpendCap — the ceiling stays HERE
//! ```
//!
//! The load order is the product guest host's (`monetize_product_wasm::LoadedGuest::load_kind`):
//! somebody must vouch, the digest, the signature before compilation, the
//! kind's ceilings narrowed by the block, the doors gate, the ABI, the
//! manifest — which must say `kind = "cloud"` and name this block. Then the
//! guest proves its credential ([`monetize_cloud_guest::ops::VERIFY`]) and
//! answers its provider name and zones once ([`monetize_cloud_guest::ops::ABOUT`]).
//!
//! # What the host keeps
//!
//! * **The spend ceiling**, always: `monetize-server`'s registry wraps this in
//!   `SpendCap` like any cloud that can buy. A guest cloud is never trusted to
//!   say it cannot spend — [`factory`] declares `spends: true`, so a block
//!   with no `cap` is refused before the module is even read.
//! * **The state**, durably: a cloud remembers its pool ledger (grants,
//!   plan-change intents) between calls, and a restart must not forget one.
//!   The doors' state map is loaded from `<data_dir>/<name>-guest-state.json`
//!   at build and written back atomically after any call that changed it.
//! * **One call at a time**: the calls into one cloud are serialised, which is
//!   what a linked provider's ledger mutex guaranteed.
//!
//! # The journaled growth
//!
//! `grow_set` and `resume_grow` cross (2026-09-26): the guest's journal writes
//! THROUGH the state door (`monetize_cloud_guest::JOURNAL_KEY`) to the host's
//! journal file, fsync'd before the guest's put returns. The pure growth ops
//! (`grow_gate`, `price_grow`, `survey_set`) are forwarded.
//!
//! # Laying a sold estate (2026-10-08)
//!
//! test2's shop answered every step of its ladder with "the upcloud cloud
//! cannot lay an estate": its cloud is this host, and the estate verbs were
//! never forwarded, so the trait's default refusal answered for a guest whose
//! provider lays one. Now `lay_estate` and `change_plan` cross journaled, the
//! way a growth does (the job's own journal file, written through); and
//! `plan_catalog`, `price_estate` and `set_power` cross plainly. A guest built
//! before them still answers by name: the estate verbs refuse as the trait
//! does, and `price_estate` prices row by row as it did.

pub mod dns;

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use guest_host::module::{Instantiated, LoadedModule};
use guest_host::wasmtime::Linker;
use monetize_cloud::cloud::{
    Boot, Booted, Catalog, Change, Changed, Cloud, CloudManifest, Give, Listed, Listing, Priced,
    RoomLeft,
};
use monetize_cloud::factory::{CloudBuild, CloudFactory};
use monetize_cloud::{grow, CloudError, Price, Reclaimed, Zone};
use monetize_cloud_guest::ops;
use monetize_cloud_guest::wire::{About, Call, EnsureReq, WireCloudError};
use monetize_product::GuestSpec;
use monetize_product_wasm::doors::{self, CallState, DoorSpec, Doors};
use monetize_product_wasm::{Block, LoadedGuest};

/// The digest `guest_digest =` pins (blake3-256, hex) — the gate's own.
pub use monetize_product_wasm::digest_hex;

/// The cloud kind's fuel ceiling: 2e10 instructions per call. An `ensure`
/// parses a price list, a label search and a few state polls; a `release_for`
/// walks an account. An operator narrows it with `guest_fuel`.
pub const CLOUD_FUEL_CEILING: u64 = 20_000_000_000;

/// The cloud kind's wall-clock ceiling: 15 minutes. A server create waits for
/// `started` (UpCloud's own poll timeout is 10 minutes by default); a call cut
/// off mid-wait leaves the resource labelled for the retry, never bought twice.
pub const CLOUD_CALL_TIMEOUT: Duration = Duration::from_secs(15 * 60);

/// **A journaled growth's wall-clock deadline**: the kind's whole ceiling, whatever the
/// block's everyday `guest_call_timeout_ms` says. A growth detaches, resizes, waits
/// for `online` (37 s per member measured on UpCloud 2026-09-14) and re-attaches
/// every member in ONE call; cut off at an everyday 30 s it would stop mid-member
/// every time (MEASURED loki 2026-09-26, the signed upcloud.wasm against the mock).
/// A call cut off anyway leaves the epoch open and `resume_grow` puts the volumes back.
pub const GROW_CALL_TIMEOUT: Duration = CLOUD_CALL_TIMEOUT;

/// **A keep boot's wall-clock deadline: 60 minutes.** The UpCloud guest waits up to 45 min for
/// the keep medium (`KEEP_BUDGET`) and then puts the box back; the host must not cut it mid-wait,
/// so it gets that wait plus 15 min. It stays under the 2 h the fronts wait on `Upgrade`
/// ([`monetize_cloud::lay::KEEP_BOOT_WITHIN`]), so the host gives up before the front does.
pub const KEEP_BOOT_CALL_TIMEOUT: Duration = Duration::from_secs(60 * 60);

/// **The deadline of one journaled call `op`**: [`KEEP_BOOT_CALL_TIMEOUT`] for a keep boot,
/// [`GROW_CALL_TIMEOUT`] for every other one (a growth, a lay, a plan change, a restore).
pub fn journaled_deadline(op: u32) -> Duration {
    if op == ops::KEEP_BOOT {
        KEEP_BOOT_CALL_TIMEOUT
    } else {
        GROW_CALL_TIMEOUT
    }
}

/// How long one HTTP request through the door may take — and the longest a
/// guest may wait on the host in one `host_sleep`.
pub const CLOUD_DOOR_TIMEOUT: Duration = Duration::from_secs(60);

/// The largest value one state key may hold for a cloud: 4 MiB. The pool
/// ledger is ONE key growing by a row per sold resource; the product kind's
/// 64 KiB would stop a deployment after a few hundred sales.
pub const CLOUD_STATE_MAX_BYTES: usize = 4 * 1024 * 1024;

/// **The keys of a `[clouds.<name>]` block that are the HOST's**: which module,
/// who vouches for it, which doors, which hosts, which credential the HTTP
/// door presents to which host (`[[clouds.<name>.http_auth]]`, read from
/// `secrets_dir` — the server fills that one in), which bounds. They are never
/// sent to the guest; everything else in the block is the guest's settings.
pub const HOST_KEYS: &[&str] = &[
    "guest",
    "guest_digest",
    "guest_trust_roots",
    "grants",
    "http_hosts",
    "http_auth",
    "http_resolve",
    "secrets_dir",
    "guest_fuel",
    "guest_call_timeout_ms",
    "guest_max_memory_bytes",
];

/// Does this block name a guest?
pub fn is_guest_block(settings: &serde_json::Value) -> bool {
    settings.get("guest").is_some_and(|v| !v.is_null())
}

/// **The block's guest keys, typed** — `None` when it names no guest.
pub fn guest_spec(name: &str, settings: &serde_json::Value) -> Result<Option<GuestSpec>, String> {
    if !is_guest_block(settings) {
        return Ok(None);
    }
    let err = |k: &str, e: String| format!("[clouds.{name}] `{k}` does not parse: {e}");
    fn get<T: serde::de::DeserializeOwned>(
        s: &serde_json::Value,
        k: &str,
    ) -> Result<Option<T>, String> {
        match s.get(k) {
            None | Some(serde_json::Value::Null) => Ok(None),
            Some(v) => serde_json::from_value(v.clone())
                .map(Some)
                .map_err(|e| e.to_string()),
        }
    }
    let module: PathBuf = get(settings, "guest")
        .map_err(|e| err("guest", e))?
        .unwrap_or_default();
    Ok(Some(GuestSpec {
        module,
        digest: get(settings, "guest_digest").map_err(|e| err("guest_digest", e))?,
        trust_roots: get(settings, "guest_trust_roots")
            .map_err(|e| err("guest_trust_roots", e))?
            .unwrap_or_default(),
        grants: get(settings, "grants")
            .map_err(|e| err("grants", e))?
            .unwrap_or_default(),
        http_hosts: get(settings, "http_hosts")
            .map_err(|e| err("http_hosts", e))?
            .unwrap_or_default(),
        grpc_hosts: Vec::new(),
        fuel: get(settings, "guest_fuel").map_err(|e| err("guest_fuel", e))?,
        call_timeout_ms: get(settings, "guest_call_timeout_ms")
            .map_err(|e| err("guest_call_timeout_ms", e))?,
        max_memory_bytes: get(settings, "guest_max_memory_bytes")
            .map_err(|e| err("guest_max_memory_bytes", e))?,
        // The provider's credential: the host sets the header per host from a
        // secret file, and the guest never sees it (the money lane's door).
        http_auth: get(settings, "http_auth")
            .map_err(|e| err("http_auth", e))?
            .unwrap_or_default(),
        // A cloud verifies no webhook.
        hmac_secrets: Vec::new(),
        secrets_dir: get(settings, "secrets_dir").map_err(|e| err("secrets_dir", e))?,
        plugin: None,
    }))
}

/// The block as the GUEST sees it: every key but [`HOST_KEYS`].
pub fn guest_settings(settings: &serde_json::Value) -> serde_json::Value {
    let mut out = serde_json::Map::new();
    if let Some(obj) = settings.as_object() {
        for (k, v) in obj {
            if !HOST_KEYS.contains(&k.as_str()) {
                out.insert(k.clone(), v.clone());
            }
        }
    }
    serde_json::Value::Object(out)
}

/// **A block's identity as far as a live reload is concerned**: the module's
/// bytes and its signature (by digest) and every key of the block. Two equal
/// fingerprints build the same cloud.
pub fn block_fingerprint(name: &str, settings: &serde_json::Value) -> Result<String, String> {
    let mut parts = format!("{name}|{settings}");
    if let Some(spec) = guest_spec(name, settings)? {
        let bytes = std::fs::read(&spec.module).map_err(|e| {
            format!(
                "[clouds.{name}] guest {}: could not be read: {e}",
                spec.module.display()
            )
        })?;
        parts.push('|');
        parts.push_str(&guest_host::digest_hex(&bytes));
        let mut sig = spec.module.clone().into_os_string();
        sig.push(".p7s");
        if let Ok(p7s) = std::fs::read(PathBuf::from(sig)) {
            parts.push('|');
            parts.push_str(&guest_host::digest_hex(&p7s));
        }
        for root in &spec.trust_roots {
            if let Ok(der) = std::fs::read(root) {
                parts.push('|');
                parts.push_str(&guest_host::digest_hex(&der));
            }
        }
    }
    Ok(guest_host::digest_hex(parts.as_bytes()))
}

/// **The factory for `[clouds.<name>] guest = …`.** `name` is the block's
/// name, leaked once per configured guest block ([`leak_name`]) because a
/// factory's name is `&'static` and a config file is not. `spends: true`,
/// always: whether a guest can buy is not the guest's to say, so the server
/// demands a ceiling for every one of them.
pub fn factory(name: &'static str) -> CloudFactory {
    CloudFactory {
        name,
        config_envs: &[],
        spends: true,
        build,
    }
}

/// A configured name as the `&'static str` a factory name is.
pub fn leak_name(name: &str) -> &'static str {
    Box::leak(name.to_string().into_boxed_str())
}

fn build(ctx: &CloudBuild<'_>) -> Result<Box<dyn Cloud>, String> {
    Ok(Box::new(GuestCloud::build(
        ctx.name,
        ctx.data_dir,
        ctx.settings.0,
    )?))
}

/// **A cloud guest, as the server holds it.**
///
/// Every op BLOCKS the calling thread for as long as the provider takes (a
/// lay: up to [`GROW_CALL_TIMEOUT`]); call it from a thread that may block —
/// `spawn_blocking`, never an async worker ([`doors::register`] says why).
pub struct GuestCloud {
    name: &'static str,
    module: LoadedModule,
    doors: Arc<Doors>,
    manifest: monetize_product_guest::Manifest,
    abi: u32,
    /// The block minus [`HOST_KEYS`], sent with every call.
    settings: serde_json::Value,
    zones: Vec<Zone>,
    ceilings: BTreeMap<String, u64>,
    /// The guest's grow switch for this block (op 230), asked once at load: the block's
    /// settings decide it, and a changed block is a new guest.
    grow_refused: Option<String>,
    /// Where the doors' state map is kept between calls and restarts.
    state_file: PathBuf,
    /// The map as last written, so an unchanged call writes nothing.
    saved: Mutex<BTreeMap<String, Vec<u8>>>,
    /// The last state write that failed, until one succeeds (see `call_in`).
    unwritten: Mutex<Option<String>>,
    /// One call at a time into this cloud — its everyday lane (prices,
    /// purchases, growths, reads).
    serial: Mutex<()>,
    /// **A sold estate's own lane** (wiring trace A32, 2026-10-08): a lay, a
    /// plan change and a power step wait minutes on the provider (servers
    /// created, stopped, started); on the everyday lane they held every room
    /// purchase of the shop back for up to 15 minutes. They run here instead,
    /// one at a time, beside the everyday lane. They touch none of the state
    /// the everyday lane writes (the pool ledger): their journal is written
    /// THROUGH to its own file, and the state map is shared key by key.
    estate_serial: Mutex<()>,
}

/// **A call's write-through slot, held for exactly that call** (code-smell
/// review 2026-10-10, row 17): installed under the call's own number and
/// taken out when this guard drops — at the end of the call, on an early
/// return, or on a panic — so a slot can never outlive its journaled call.
struct ThroughSlot<'a> {
    doors: &'a doors::Doors,
    call: u64,
}

impl<'a> ThroughSlot<'a> {
    fn hold(doors: &'a doors::Doors, call: u64, through: doors::WriteThrough) -> Self {
        doors
            .write_through
            .lock()
            .unwrap_or_else(|p| p.into_inner())
            .insert(call, through);
        ThroughSlot { doors, call }
    }
}

impl Drop for ThroughSlot<'_> {
    fn drop(&mut self) {
        self.doors
            .write_through
            .lock()
            .unwrap_or_else(|p| p.into_inner())
            .remove(&self.call);
    }
}

/// Which lane a call takes ([`GuestCloud::estate_serial`]).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Lane {
    Everyday,
    Estate,
}

impl std::fmt::Debug for GuestCloud {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("GuestCloud")
            .field("name", &self.name)
            .field("module", &self.module.path())
            .field("digest", &self.module.digest_hex())
            .field("signer", &self.module.signer())
            .field("abi", &self.abi)
            .field("grants", &self.manifest.grants)
            .finish()
    }
}

/// The durable state file: every key, as text when it is text (a pool ledger
/// is JSON and an operator should be able to read it), else as bytes.
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct StateFile {
    version: u32,
    keys: BTreeMap<String, serde_json::Value>,
}

fn state_path(data_dir: &Path, name: &str) -> PathBuf {
    data_dir.join(format!("{name}-guest-state.json"))
}

fn load_state(data_dir: &Path, name: &str) -> Result<BTreeMap<String, Vec<u8>>, String> {
    let path = state_path(data_dir, name);
    match std::fs::read(&path) {
        Ok(bytes) => {
            let f: StateFile = serde_json::from_slice(&bytes).map_err(|e| format!("[clouds.{name}] guest state {} is unreadable: {e}; refused rather than started from nothing, which could sell the same resource twice", path.display()))?;
            let mut out = BTreeMap::new();
            for (k, v) in f.keys {
                let b = match v {
                    serde_json::Value::String(s) => s.into_bytes(),
                    other => serde_json::from_value::<Vec<u8>>(other).map_err(|e| {
                        format!(
                            "[clouds.{name}] guest state {} key {k:?}: {e}",
                            path.display()
                        )
                    })?,
                };
                out.insert(k, b);
            }
            Ok(out)
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            // **Continuity from a linked provider.** The linked UpCloud kept its
            // pool ledger at `<data_dir>/<name>-pool.json`; a deployment moving
            // the same cloud to a guest must not forget a grant or an intent.
            // Only a ledger that is NOT THERE means "start empty": one that cannot be
            // read would otherwise start empty and sell again what was sold.
            let mut out = BTreeMap::new();
            let pool = data_dir.join(format!("{name}-pool.json"));
            match std::fs::read(&pool) {
                Ok(bytes) => {
                    out.insert("pool".to_string(), bytes);
                }
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
                Err(e) => return Err(format!("[clouds.{name}] the linked provider's pool ledger {} cannot be read: {e}; refused rather than started with an empty ledger", pool.display())),
            }
            Ok(out)
        }
        Err(e) => Err(format!(
            "[clouds.{name}] guest state {}: {e}",
            path.display()
        )),
    }
}

fn save_state(path: &Path, state: &BTreeMap<String, Vec<u8>>) -> Result<(), CloudError> {
    let f = StateFile {
        version: 1,
        keys: state
            .iter()
            .map(|(k, v)| {
                let j = match std::str::from_utf8(v) {
                    Ok(s) => serde_json::Value::String(s.to_string()),
                    // Bytes that are not UTF-8 are kept as an array of numbers, built
                    // directly so no failure can turn them into `null` (row 23).
                    Err(_) => serde_json::Value::Array(
                        v.iter().map(|b| serde_json::Value::from(*b)).collect(),
                    ),
                };
                (k.clone(), j)
            })
            .collect(),
    };
    let bytes = serde_json::to_vec_pretty(&f)
        .map_err(|e| CloudError::Unavailable(format!("guest state encode: {e}")))?;
    monetize_cloud::pool::write_atomically(path, &bytes, "cloud guest state")
}

impl GuestCloud {
    /// The whole load order, the doors, the state, then `verify` and `about`.
    pub fn build(
        name: &str,
        data_dir: &Path,
        settings: &serde_json::Value,
    ) -> Result<GuestCloud, String> {
        let spec = guest_spec(name, settings)?.ok_or_else(|| format!("[clouds.{name}] is built by the cloud guest host but names no `guest = \"<path>.wasm\"`"))?;
        let block_err = |what: String| format!("[clouds.{name}] {what}");
        let block = Block {
            section: "clouds",
            id: name,
            spec: Some(&spec),
            kind: monetize_product_guest::kinds::CLOUD,
        };
        let loaded = LoadedGuest::load_block_within(block, CLOUD_FUEL_CEILING, CLOUD_CALL_TIMEOUT)?;
        if loaded.manifest.identity {
            return Err(block_err(format!(
                "its guest ({}) declares an identity (the `sign` grant); a cloud signs nothing, and a key minted for it would be a key nobody verifies",
                loaded.module.path().display()
            )));
        }
        let call_timeout = loaded.module.limits().call_timeout.min(CLOUD_CALL_TIMEOUT);
        let secrets_dir = spec
            .secrets_dir
            .clone()
            .unwrap_or_else(|| data_dir.join("secrets"));
        // `http_resolve` (a HOST key): where the door connects for a host name —
        // the bench's vm1 reaches its mock cloud (a loopback name to the guest)
        // at the mock network's relay address.
        let resolve: Vec<String> = match settings.get("http_resolve") {
            None | Some(serde_json::Value::Null) => Vec::new(),
            Some(v) => serde_json::from_value(v.clone())
                .map_err(|e| format!("[clouds.{name}] `http_resolve` does not parse: {e}"))?,
        };
        monetize_product_wasm::doors::resolve_pairs(&resolve)
            .map_err(|e| format!("[clouds.{name}] {e}"))?;
        let mut doors = Doors::open_with(DoorSpec {
            product: name,
            endpoint: "",
            bearer: None,
            twin: None,
            tls_ca: None,
            tls_client: None,
            grants: &loaded.spec.grants,
            http_hosts: &loaded.spec.http_hosts,
            grpc_hosts: &[],
            call_timeout: CLOUD_DOOR_TIMEOUT.min(call_timeout),
            http_auth: &loaded.spec.http_auth,
            hmac_secrets: &[],
            secrets_dir: Some(&secrets_dir),
            http_resolve: &resolve,
        })
        .map_err(&block_err)?;
        doors.state_max_bytes = CLOUD_STATE_MAX_BYTES;
        let initial = load_state(data_dir, name)?;
        *doors.state.lock().unwrap_or_else(|p| p.into_inner()) = initial.clone();
        let mut g = GuestCloud {
            name: leak_name(name),
            module: loaded.module,
            doors: Arc::new(doors),
            manifest: loaded.manifest,
            abi: loaded.abi,
            settings: guest_settings(settings),
            zones: Vec::new(),
            ceilings: BTreeMap::new(),
            grow_refused: None,
            state_file: state_path(data_dir, name),
            saved: Mutex::new(initial),
            unwritten: Mutex::new(None),
            serial: Mutex::new(()),
            estate_serial: Mutex::new(()),
        };
        g.call::<(), ()>(ops::VERIFY, &()).map_err(|e| {
            block_err(format!(
                "its guest could not prove it can reach the provider: {e}"
            ))
        })?;
        let about: About = g
            .call(ops::ABOUT, &())
            .map_err(|e| block_err(e.to_string()))?;
        if about.provider != name {
            return Err(block_err(format!(
                "its guest's provider answers `{}`, not `{name}`: its purchases would be recorded under another cloud's name and ceiling; refused",
                about.provider
            )));
        }
        g.zones = about.zones;
        g.ceilings = about.ceilings;
        g.grow_refused = g
            .call::<_, ()>(ops::GROW_GATE, &())
            .err()
            .map(|e| e.to_string());
        Ok(g)
    }

    pub fn digest_hex(&self) -> &str {
        self.module.digest_hex()
    }
    pub fn signer(&self) -> Option<&str> {
        self.module.signer()
    }
    pub fn abi(&self) -> u32 {
        self.abi
    }
    /// Exactly the doors defined on every linker this guest makes.
    pub fn linker_defines(&self) -> &[&'static str] {
        &self.doors.granted_imports
    }
    pub fn state_file(&self) -> &Path {
        &self.state_file
    }

    fn instantiate(&self) -> Result<Instantiated<CallState>, String> {
        let mut linker = Linker::new(self.module.engine());
        doors::register(&mut linker, &self.doors.granted_imports)?;
        let state = CallState::new(Arc::clone(&self.doors), self.module.limits().limiter());
        self.module
            .instantiate(state, &linker)
            .map_err(|e| e.to_string())
    }

    /// **One op**: serialised, the block's settings with it, under the bounds;
    /// the state written back when it moved. A HOST-side failure (a bound, a
    /// trap, a door fault, a shape) is `Unavailable` naming the module and the
    /// op — nothing is known to have been bought, and the retry's label search
    /// finds anything that was. The guest's own answer keeps its kind.
    fn call<A: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        op: u32,
        args: &A,
    ) -> Result<T, CloudError> {
        self.call_within(op, args, None)
    }

    /// [`GuestCloud::call`] with a longer wall-clock deadline for THIS call (never a
    /// shorter one): the store's epoch deadline is raised to cover `deadline`.
    fn call_within<A: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        op: u32,
        args: &A,
        deadline: Option<Duration>,
    ) -> Result<T, CloudError> {
        self.call_in(Lane::Everyday, op, args, deadline)
    }

    /// [`GuestCloud::call_within`] on a named lane.
    fn call_in<A: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        lane: Lane,
        op: u32,
        args: &A,
        deadline: Option<Duration>,
    ) -> Result<T, CloudError> {
        self.try_in(lane, op, args, deadline)
            .map_err(CloudError::from)
    }

    /// [`GuestCloud::call_in`], a guest that predates `op` answered as [`Fault::Predates`].
    fn try_in<A: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        lane: Lane,
        op: u32,
        args: &A,
        deadline: Option<Duration>,
    ) -> Result<T, Fault> {
        let asked = std::time::Instant::now();
        let _one = self.lane(lane);
        self.call_held(op, args, deadline, None, asked)
    }

    /// The one-at-a-time lock of `lane`.
    fn lane(&self, lane: Lane) -> std::sync::MutexGuard<'_, ()> {
        match lane {
            Lane::Everyday => self.serial.lock().unwrap_or_else(|p| p.into_inner()),
            Lane::Estate => self.estate_serial.lock().unwrap_or_else(|p| p.into_inner()),
        }
    }

    /// [`GuestCloud::call_in`] with its lane already held (since `asked`), and
    /// `through` installed as THIS call's write-through slot for its length.
    fn call_held<A: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        op: u32,
        args: &A,
        deadline: Option<Duration>,
        through: Option<doors::WriteThrough>,
        asked: std::time::Instant,
    ) -> Result<T, Fault> {
        let queued = asked.elapsed();
        // A slow op is said with where its time went — queued behind another
        // op on this guest's one lock (a growth holds it for minutes), or at
        // the provider (each of its HTTP calls says its own line). test2
        // 2026-09-30: a signup waited 25 s on ops nobody could see.
        let _said = SlowOp {
            name: self.name,
            op,
            asked,
            queued,
        };
        let unavailable = |e: String| {
            CloudError::Unavailable(format!(
                "[clouds.{}] guest {}: op {op} ({}): {e}",
                self.name,
                self.module.path().display(),
                ops::name(op).unwrap_or("?")
            ))
        };
        let payload = monetize_cloud_guest::wire::encode(&Call {
            name: self.name.to_string(),
            settings: self.settings.clone(),
            args,
        });
        let limits = self.module.limits();
        let raw = self
            .instantiate()
            .map_err(unavailable)
            .and_then(|mut inst| {
                if let Some(d) = deadline.filter(|d| *d > limits.call_timeout) {
                    let (ticks, period) = limits.epoch_schedule();
                    let want = (d.as_millis() / period.as_millis().max(1)) as u64;
                    inst.store.set_epoch_deadline(want.max(ticks));
                }
                let call = inst.store.data().call;
                let _slot = through.map(|w| ThroughSlot::hold(&self.doors, call, w));
                monetize_product_wasm::counters::timed(
                    monetize_product_wasm::counters::kind::CLOUD,
                    &monetize_product_wasm::counters::guest_label(self.module.path()),
                    op,
                    ops::name(op).unwrap_or("?"),
                    || self.module.call_op(&mut inst, op, &payload),
                )
                .map_err(|e| unavailable(e.to_string()))
                .map(|raw| (raw, call))
            });
        // Only THIS call's streams: another lane's call may be running beside it.
        let left = match &raw {
            Ok((_, call)) => self.doors.sweep_streams_of(*call),
            Err(_) => 0,
        };
        let raw = raw.map(|(r, _)| r);
        if left > 0 {
            eprintln!("[clouds.{}] guest: {left} stream(s) were still open when the call ended; each was cancelled", self.name);
        }
        // The state is written back whatever the answer: a ledger row written
        // before a failure is exactly the write-ahead intent a retry needs.
        // A write that fails NEVER replaces the guest's answer: "server
        // created" turned into Unavailable would tell the caller nothing was
        // bought while the provider bills it. The failure is said loudly and
        // held in `state_unwritten` until a later write succeeds; `saved` is
        // left as it was, so the next call writes the state again.
        match self.persist() {
            Ok(()) => *self.unwritten.lock().unwrap_or_else(|p| p.into_inner()) = None,
            Err(e) => {
                eprintln!(
                    "[clouds.{}] ALERT cloud-guest.state-unwritten guest state NOT written after op {} ({}); the guest's answer is returned as it was, the write is retried on the next call: {e}",
                    self.name,
                    op,
                    ops::name(op).unwrap_or("?")
                );
                *self.unwritten.lock().unwrap_or_else(|p| p.into_inner()) = Some(e.to_string());
            }
        }
        let raw = raw?;
        let answer: Result<T, WireCloudError> = monetize_cloud_guest::wire::decode(&raw)
            .map_err(|e| unavailable(format!("the guest answered undecodable bytes: {e}")))?;
        answer.map_err(|e| {
            if Self::reads_as_predating(op, &e) {
                Fault::Predates(op)
            } else {
                Fault::Cloud(e.into())
            }
        })
    }

    /// **Why the guest's state is not on disk**, if the last write failed. A
    /// call whose state write failed still returns the guest's answer; this is
    /// how a caller (or a health check) sees that the file is behind.
    pub fn state_unwritten(&self) -> Option<String> {
        self.unwritten
            .lock()
            .unwrap_or_else(|p| p.into_inner())
            .clone()
    }

    fn persist(&self) -> Result<(), CloudError> {
        // The written-back lock is taken BEFORE the map is read: with two lanes
        // (A32) a persist that read an older map could otherwise write it over
        // a newer one written in between — the file would lose a grant.
        let mut saved = self.saved.lock().unwrap_or_else(|p| p.into_inner());
        let now = self
            .doors
            .state
            .lock()
            .unwrap_or_else(|p| p.into_inner())
            .clone();
        if *saved != now {
            save_state(&self.state_file, &now)?;
            *saved = now;
        }
        Ok(())
    }

    /// **A journaled growth across the ABI.** The guest runs the provider's own
    /// `grow_set` / `resume_grow` against the journal as `journal` holds it; each
    /// record it journals arrives as a `state_put` of
    /// [`monetize_cloud_guest::JOURNAL_KEY`], which the doors hand THROUGH to
    /// `journal`'s own file (append + fsync) before the guest's put returns —
    /// the write-ahead rule a linked provider keeps, kept across the boundary.
    /// After the call `journal` re-reads its file, so the caller sees every step.
    /// A journal with no file (nothing to write through to) is refused.
    fn journaled_call(
        &self,
        op: u32,
        set: &grow::DataSet,
        journal: &grow::Journal,
    ) -> Result<grow::GrowOutcome, CloudError> {
        self.journaled_op(op, journal, |records| monetize_cloud_guest::wire::GrowReq {
            set: set.clone(),
            records,
        })
    }

    /// **Any journaled op across the ABI** — a growth, a lay, a plan change:
    /// the request is built over the journal's records as `journal` holds
    /// them, every record the guest journals is written THROUGH to
    /// `journal`'s file before the guest's put returns, and the call may run
    /// the kind's whole deadline. `journal` re-reads its file afterwards.
    fn journaled_op<R: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        op: u32,
        journal: &grow::Journal,
        req: impl FnOnce(Vec<grow::GrowRecord>) -> R,
    ) -> Result<T, CloudError> {
        self.journaled_in(Lane::Everyday, op, journal, req)
    }

    /// [`GuestCloud::journaled_op`] on a named lane.
    fn journaled_in<R: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        lane: Lane,
        op: u32,
        journal: &grow::Journal,
        req: impl FnOnce(Vec<grow::GrowRecord>) -> R,
    ) -> Result<T, CloudError> {
        self.try_journaled_in(lane, op, journal, req)
            .map_err(CloudError::from)
    }

    /// [`GuestCloud::journaled_in`], a guest that predates `op` answered as [`Fault::Predates`].
    fn try_journaled_in<R: serde::Serialize, T: serde::de::DeserializeOwned>(
        &self,
        lane: Lane,
        op: u32,
        journal: &grow::Journal,
        req: impl FnOnce(Vec<grow::GrowRecord>) -> R,
    ) -> Result<T, Fault> {
        if journal.path().as_os_str().is_empty() {
            return Err(self.journaled(ops::name(op).unwrap_or("?")).into());
        }
        // The lane is held across the whole call — the journal's records read,
        // the call, the reload — so two journaled calls on one lane never
        // interleave; a call on the other lane runs beside it, with its own
        // write-through slot.
        let asked = std::time::Instant::now();
        let _one = self.lane(lane);
        let file = Arc::new(grow::Journal::open(journal.path())?);
        let name = self.name;
        let sink = {
            let file = Arc::clone(&file);
            move |bytes: &[u8]| -> Result<(), String> {
                let rec: grow::GrowRecord = serde_json::from_slice(bytes).map_err(|e| {
                    format!("[clouds.{name}] a journal record that does not parse: {e}")
                })?;
                file.append(rec).map_err(|e| e.to_string())
            }
        };
        let through = doors::WriteThrough {
            key: monetize_cloud_guest::JOURNAL_KEY.to_string(),
            sink: Box::new(sink),
        };
        let out = self.call_held(
            op,
            &req(journal.records()),
            Some(journaled_deadline(op)),
            Some(through),
            asked,
        );
        journal.reload()?;
        out
    }

    /// **Does the guest's answer to `op` say it predates `op`?** Its SDK's
    /// typed [`WireCloudError::NotInTable`] for this op — or, from a guest
    /// built before that variant, the SDK's own sentence for this op, EXACTLY
    /// (stinky upcloud #21, 5.4). A refusal that merely quotes the words is the
    /// guest's own.
    fn reads_as_predating(op: u32, e: &WireCloudError) -> bool {
        match e {
            WireCloudError::NotInTable(n) => *n == op,
            WireCloudError::Refused(m) => *m == ops::not_in_table(op),
            _ => false,
        }
    }

    /// The refusal of an estate verb by a guest that predates it: the trait's
    /// own words (so every reader that knows them still does), and which
    /// module said it.
    fn estate_refusal(&self, verb: &str) -> CloudError {
        let base = if verb == "lay_estate" || verb == "plan_catalog" {
            monetize_cloud::lay::unsupported(self.name)
        } else {
            monetize_cloud::lay::unsupported_verb(self.name, verb)
        };
        let CloudError::Refused(words) = base else {
            return base;
        };
        CloudError::Refused(format!(
            "{words} (its guest {} predates `{verb}`; a newer signed guest carries it)",
            self.module.digest_hex()
        ))
    }

    fn journaled(&self, verb: &str) -> CloudError {
        CloudError::Refused(format!(
            "[clouds.{}] `{verb}` is not carried across the cloud guest ABI here: a \
             growth, a lay or a plan change crosses only with a journal FILE the guest's steps are written through to (this journal has none). \
             Nothing was touched",
            self.name
        ))
    }
}

/// **What a guest call failed with**: the guest's answer that it predates the
/// op (read by kind, [`GuestCloud::reads_as_predating`]), or any other error.
/// Never leaves this crate: [`CloudError::from`] says a `Predates` in the SDK's
/// own words, as a refusal, where a caller asked with no fallback of its own.
enum Fault {
    Predates(u32),
    Cloud(CloudError),
}

impl From<CloudError> for Fault {
    fn from(e: CloudError) -> Self {
        Fault::Cloud(e)
    }
}

impl From<Fault> for CloudError {
    fn from(f: Fault) -> Self {
        match f {
            Fault::Predates(op) => CloudError::Refused(ops::not_in_table(op)),
            Fault::Cloud(e) => e,
        }
    }
}

/// An op past this is said when it ends.
pub const SLOW_OP: Duration = Duration::from_secs(1);

struct SlowOp {
    name: &'static str,
    op: u32,
    asked: std::time::Instant,
    queued: Duration,
}

impl Drop for SlowOp {
    fn drop(&mut self) {
        let took = self.asked.elapsed();
        if took >= SLOW_OP {
            eprintln!(
                "[clouds.{}] op {} took {} ms ({} ms of it queued behind another op on this guest)",
                self.name,
                ops::name(self.op).unwrap_or("?"),
                took.as_millis(),
                self.queued.as_millis()
            );
        }
    }
}

/// **The nine ops over the guest's op table** (A24 §2.5) — the host-side adapter, ONCE.
/// Every kind keeps the op number, the lane and the deadline it always had, so a signed
/// guest built before keeps serving; a guest that predates an op is read by KIND: priced
/// row by row (`price` of a plan), no address of its own (`catalog.ipv4`), no media to give
/// back, or refused by name (the rest).
impl Cloud for GuestCloud {
    /// The grow switch is the guest's own answer for this block's settings (op 230, asked
    /// at load), so a manifest is no guest call.
    fn manifest(&self) -> CloudManifest {
        let settings = self
            .settings
            .as_object()
            .map(|o| o.keys().cloned().collect())
            .unwrap_or_default();
        CloudManifest {
            provider: self.name.to_string(),
            zones: self.zones.clone(),
            settings,
            grow_refused: self.grow_refused.clone(),
        }
    }
    fn catalog(&self, zone: &Zone) -> Result<Catalog, CloudError> {
        let plans = match self.try_in(Lane::Everyday, ops::PLAN_CATALOG, zone, None) {
            Err(Fault::Predates(_)) => Err(self.estate_refusal("plan_catalog")),
            other => other.map_err(CloudError::from),
        }?;
        // A guest built before PRICE_IPV4 (the signed upcloud.wasm of 2026-10-05 and
        // older) prices no address: the room price base counts its stated fallback.
        let ipv4 = self.price_ipv4(zone)?;
        Ok(Catalog { plans, ipv4 })
    }
    /// A plan: the provider's own estate price (UpCloud: every disk at its TIER, read from
    /// a price list fetched now); a guest that predates the op is priced row by row. A
    /// growth and a resource: the guest's own list (ops 231, 210).
    fn price(&self, what: &Priced<'_>) -> Result<Price, CloudError> {
        match what {
            Priced::Plan(plan) => {
                match self.try_in::<_, Price>(Lane::Everyday, ops::PRICE_ESTATE, *plan, None) {
                    Err(Fault::Predates(_)) => monetize_cloud::price_estate_by_rows(
                        plan,
                        |r| self.call(ops::PRICE, r),
                        self.price_ipv4(&plan.zone)?,
                    ),
                    other => other.map_err(CloudError::from),
                }
            }
            Priced::Grow(set) => self.call(ops::PRICE_GROW, *set),
            Priced::Resource(r) => self.call(ops::PRICE, *r),
        }
    }
    /// **Journaled, like a growth**: every disk and server the guest creates is an intent
    /// in the job's journal before the provider is asked, and confirmed with its id after
    /// — written through to the file. The call may run the kind's whole deadline (8 disks
    /// and 3 servers, each waited for); one cut off is `Unavailable`, and the job's retry
    /// ADOPTS by label.
    fn lay(
        &self,
        plan: &monetize_cloud::lay::EstatePlan,
        journal: &grow::Journal,
    ) -> Result<monetize_cloud::lay::LaidEstate, CloudError> {
        match self.try_journaled_in(Lane::Estate, ops::LAY_ESTATE, journal, |records| {
            monetize_cloud_guest::wire::LayReq {
                plan: plan.clone(),
                records,
            }
        }) {
            Err(Fault::Predates(_)) => Err(self.estate_refusal("lay_estate")),
            other => other.map_err(CloudError::from),
        }
    }
    fn change(&self, change: &Change, journal: &grow::Journal) -> Result<Changed, CloudError> {
        match change {
            Change::Plan {
                server,
                plan,
                delta,
            } => match self.try_journaled_in::<_, ()>(
                Lane::Estate,
                ops::CHANGE_PLAN,
                journal,
                |records| monetize_cloud_guest::wire::PlanReq {
                    server: server.clone(),
                    plan: plan.clone(),
                    delta: *delta,
                    records,
                },
            ) {
                Err(Fault::Predates(_)) => Err(self.estate_refusal("change_plan")),
                other => other.map(|()| Changed::Done).map_err(CloudError::from),
            },
            // A SOLD estate's growth: the same guest op, on the estate's lane — its detach,
            // resize and attach never hold the shop's purchases back.
            Change::Grow { set, estate: true } => self
                .journaled_in(Lane::Estate, ops::GROW_SET, journal, |records| {
                    monetize_cloud_guest::wire::GrowReq {
                        set: set.clone(),
                        records,
                    }
                })
                .map(Changed::Grown),
            Change::Grow { set, estate: false } => self
                .journaled_call(ops::GROW_SET, set, journal)
                .map(Changed::Grown),
            Change::Settle { set } => self
                .journaled_call(ops::RESUME_GROW, set, journal)
                .map(Changed::Grown),
            // **Journaled**: each clone ejected and deleted is written through as it goes. A
            // guest that predates the op laid no medium clone, so there is nothing to give back.
            Change::ForgetMedia { laid } => match self.try_journaled_in::<_, Vec<String>>(
                Lane::Estate,
                ops::FORGET_MEDIA,
                journal,
                |records| monetize_cloud_guest::wire::ForgetReq {
                    laid: laid.clone(),
                    records,
                },
            ) {
                Err(Fault::Predates(_)) => Ok(Changed::Forgotten(Vec::new())),
                other => other.map(Changed::Forgotten).map_err(CloudError::from),
            },
            Change::Ensure {
                reference,
                resource,
                room,
            } => {
                let op = if room.is_some() {
                    ops::ENSURE_WITH
                } else {
                    ops::ENSURE
                };
                self.call(
                    op,
                    &EnsureReq {
                        reference: reference.clone(),
                        resource: resource.clone(),
                        room: *room,
                    },
                )
                .map(Changed::Ensured)
            }
            // On the estate lane. A guest that predates the op labels nothing: refused by name (the
            // adopt says so and goes on; the inventory looks the machines up by uuid).
            Change::LabelAdopted { laid } => match self.try_in::<_, Vec<String>>(
                Lane::Estate,
                ops::LABEL_ADOPTED,
                laid,
                Some(CLOUD_CALL_TIMEOUT),
            ) {
                Err(Fault::Predates(_)) => Err(self.estate_refusal("label_adopted")),
                other => other.map(Changed::Labelled).map_err(CloudError::from),
            },
            // The fence: what stands, each given back through the guest's own destroy.
            Change::Retire { reference, machine } => {
                monetize_cloud::cloud::retire_by_rows(self, reference, machine)
                    .map(Changed::Retired)
            }
        }
    }
    /// On the estate lane. A keep boot and a restore are **journaled** (an upgrade waits
    /// on the medium for minutes); a stop waits for `stopped` the kind's whole deadline,
    /// never the block's everyday one.
    fn boot(&self, boot: &Boot, journal: &grow::Journal) -> Result<Booted, CloudError> {
        match boot {
            Boot::Keep(keep) => match self.try_journaled_in::<_, monetize_cloud::lay::LaidMachine>(
                Lane::Estate,
                ops::KEEP_BOOT,
                journal,
                |records| monetize_cloud_guest::wire::KeepBootReq {
                    keep: (**keep).clone(),
                    records,
                },
            ) {
                Err(Fault::Predates(_)) => Err(self.estate_refusal("keep_boot")),
                other => other.map(Booted::Machine).map_err(CloudError::from),
            },
            Boot::Power { server, on } => match self.try_in::<_, ()>(
                Lane::Estate,
                ops::SET_POWER,
                &monetize_cloud_guest::wire::PowerReq {
                    server: server.clone(),
                    on: *on,
                },
                Some(CLOUD_CALL_TIMEOUT),
            ) {
                Err(Fault::Predates(_)) => Err(self.estate_refusal("set_power")),
                other => other.map(|()| Booted::Powered).map_err(CloudError::from),
            },
            Boot::Restore(restore) => match self
                .try_journaled_in::<_, Option<monetize_cloud::lay::Restored>>(
                    Lane::Estate,
                    ops::RESTORE_FROM_BACKUP,
                    journal,
                    |records| monetize_cloud_guest::wire::RestoreFromBackupReq {
                        restore: (**restore).clone(),
                        records,
                    },
                ) {
                Err(Fault::Predates(_)) => Err(self.estate_refusal("restore_from_backup")),
                other => other.map(Booted::Restored).map_err(CloudError::from),
            },
        }
    }
    /// On the estate lane. A guest that predates the op writes no firewall: refused by name
    /// (the caller says so and goes on — the estate keeps the rules it has).
    fn firewall(&self, laid: &monetize_cloud::lay::LaidEstate) -> Result<(), CloudError> {
        match self.try_in::<_, ()>(
            Lane::Estate,
            ops::SET_ESTATE_FIREWALL,
            laid,
            Some(CLOUD_CALL_TIMEOUT),
        ) {
            Err(Fault::Predates(_)) => Err(self.estate_refusal("set_estate_firewall")),
            other => other.map_err(CloudError::from),
        }
    }
    /// **One resource: a destroy waits for the provider, the kind's whole deadline, never
    /// the block's everyday one** (F4, the ONE box 2026-10-20). UpCloud deletes no STARTED
    /// server, so the guest stops it hard, waits for `stopped`, then deletes it with its
    /// disks — tens of seconds. Cut off at the everyday 30 s, test3-holger-rs's main was left
    /// STOPPED and billed. On the estate's lane, as a stop is, so a slow give-back holds no
    /// purchase of the shop back.
    fn destroy(&self, what: &Give<'_>) -> Result<Reclaimed, CloudError> {
        match what {
            Give::References(references) => self.call(ops::RELEASE_FOR, references),
            Give::Resource(id) => {
                self.call_in::<_, ()>(Lane::Estate, ops::DESTROY, *id, Some(CLOUD_CALL_TIMEOUT))?;
                Ok(Reclaimed {
                    destroyed: vec![(*id).clone()],
                    ..Reclaimed::default()
                })
            }
        }
    }
    fn inventory(&self, what: &Listing<'_>) -> Result<Listed, CloudError> {
        Ok(match what {
            Listing::Account => Listed::Account(self.call(ops::INVENTORY, &())?),
            Listing::Set(set) => Listed::Set(self.call(ops::SURVEY_SET, *set)?),
            Listing::Status(id) => Listed::Status(self.call(ops::STATUS, *id)?),
            Listing::Under(references) => Listed::Under(self.call(ops::ORPHANS_FOR, references)?),
            // A guest has no admission of its own across the wire; the host's cap answers.
            Listing::Admits(_) => Listed::Admitted,
            // The guest's own word on whether it can serve (op 216); its ceilings were
            // asked once at load (a declaration, not a measurement); a guest cloud buys,
            // so it has no byte wall of its own.
            Listing::Room { delta, .. } => Listed::Room(RoomLeft {
                // An empty delta asks only the figures: no guest call.
                serve: if delta.is_empty() {
                    Ok(())
                } else {
                    self.call::<_, ()>(ops::CAN_SERVE, delta)
                        .map_err(|e| match e {
                            CloudError::Refused(s) => s,
                            other => other.to_string(),
                        })
                },
                left_bytes: None,
                ceilings: self.ceilings.clone(),
            }),
        })
    }
}

impl GuestCloud {
    /// A guest built before PRICE_IPV4 prices no address of its own.
    fn price_ipv4(&self, zone: &Zone) -> Result<Option<Price>, CloudError> {
        match self.try_in::<_, Option<Price>>(Lane::Everyday, ops::PRICE_IPV4, zone, None) {
            Err(Fault::Predates(_)) => Ok(None),
            other => other.map_err(CloudError::from),
        }
    }
}

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

    /// ★ F-U-01 (A17): a keep boot is never cut by the host before the guest's own wait for the
    /// keep medium (UpCloud's 45 min) has run out, plus a margin for its put-back; and the host
    /// still gives up before the fronts stop waiting on `Upgrade` (`KEEP_BOOT_WITHIN`, 2 h).
    /// Every other journaled call keeps the kind's 15 min.
    #[test]
    fn a_keep_boot_outlasts_the_guests_wait_for_the_keep_medium_and_the_rest_keep_fifteen_minutes()
    {
        let guest_wait = Duration::from_secs(45 * 60);
        let keep = journaled_deadline(ops::KEEP_BOOT);
        assert!(
            keep >= guest_wait + Duration::from_secs(10 * 60),
            "keep boot deadline {keep:?} leaves the guest's 45 min no margin"
        );
        assert!(
            keep < monetize_cloud::lay::KEEP_BOOT_WITHIN,
            "the host gives up before the front does: {keep:?}"
        );
        for op in [
            ops::GROW_SET,
            ops::LAY_ESTATE,
            ops::CHANGE_PLAN,
            ops::FORGET_MEDIA,
            ops::RESTORE_FROM_BACKUP,
        ] {
            assert_eq!(
                journaled_deadline(op),
                Duration::from_secs(15 * 60),
                "op {op}"
            );
        }
    }

    /// ★ Row 23 (code-smell review 2026-10-10): state bytes that are not UTF-8
    /// are written as an array built directly — never a fallible conversion
    /// that could leave `null` — and read back to the same bytes.
    #[test]
    fn guest_state_that_is_not_utf8_survives_a_save_and_a_load() {
        let dir = std::env::temp_dir().join(format!(
            "cloud-wasm-state-{}-{}",
            std::process::id(),
            line!()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let mut state = BTreeMap::new();
        state.insert("bin".to_string(), vec![0xff, 0x00, 0xfe, 0x80]);
        state.insert("text".to_string(), b"plain".to_vec());
        save_state(&state_path(&dir, "c"), &state).unwrap();
        let back = load_state(&dir, "c").unwrap();
        let _ = std::fs::remove_dir_all(&dir);
        assert_eq!(back, state);
    }

    /// ★ Row 17 (code-smell review 2026-10-10): a call's write-through slot
    /// goes when its call goes, even when the call panics — a slot left behind
    /// would hand a later call's journal writes to a dead sink.
    #[test]
    fn a_write_through_slot_is_gone_after_a_panic_in_its_call() {
        let d = Doors::open(
            "cloud",
            "",
            None,
            None,
            None,
            &["state".to_string()],
            &[],
            &[],
            Duration::from_secs(5),
        )
        .unwrap();
        let hit = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let _slot = ThroughSlot::hold(
                &d,
                42,
                doors::WriteThrough {
                    key: "grow-journal".into(),
                    sink: Box::new(|_: &[u8]| Ok(())),
                },
            );
            assert!(d.write_through.lock().unwrap().contains_key(&42));
            panic!("the call dies");
        }));
        assert!(hit.is_err());
        assert!(
            d.write_through
                .lock()
                .unwrap_or_else(|p| p.into_inner())
                .is_empty(),
            "the slot went with its call"
        );
        d.state_put_for(42, "grow-journal", b"x".to_vec()).unwrap();
        assert_eq!(
            d.state_get("grow-journal").unwrap().1,
            b"x",
            "an ordinary state write again"
        );
    }

    /// ★ A29 (wiring trace, 2026-10-08): a guest built before ops 240-244
    /// answers an estate op with its SDK's "not in this cloud guest's op
    /// table"; the host reads exactly that as "predates" — the estate verbs
    /// then refuse with the trait's own words, and the price falls back to
    /// row-by-row — while any other refusal (the provider's, the guest's own
    /// "no `set_power`") stays what it said.
    #[test]
    fn an_estate_op_an_older_guest_lacks_is_read_as_predating_it_and_nothing_else_is() {
        for op in [
            ops::PRICE_ESTATE,
            ops::PLAN_CATALOG,
            ops::LAY_ESTATE,
            ops::SET_POWER,
            ops::CHANGE_PLAN,
        ] {
            assert!(
                GuestCloud::reads_as_predating(
                    op,
                    &WireCloudError::Refused(format!("op {op} {}", ops::NOT_IN_TABLE))
                ),
                "op {op}"
            );
            assert!(
                GuestCloud::reads_as_predating(op, &WireCloudError::NotInTable(op)),
                "op {op}, typed"
            );
        }
        assert!(
            !GuestCloud::reads_as_predating(
                ops::SET_POWER,
                &WireCloudError::Refused("the toycloud cloud has no `set_power`".into())
            ),
            "the guest's own refusal is its own"
        );
        assert!(
            !GuestCloud::reads_as_predating(
                242,
                &WireCloudError::Unavailable(format!("op 242 {}", ops::NOT_IN_TABLE))
            ),
            "an outage is never a missing op"
        );
    }

    /// ★ **"Predates" is a TYPED answer, never a sentence a guest happens to
    /// say** (stinky upcloud #21, 5.4): a guest's own refusal that quotes the
    /// op-table words is its refusal; only the SDK's typed `NotInTable` — or an
    /// OLD guest's exact SDK sentence for the op asked — is "predates".
    #[test]
    fn predates_is_the_typed_answer_or_an_old_guests_exact_sentence_and_nothing_that_quotes_it() {
        let op = ops::SET_POWER;
        let quoting = WireCloudError::Refused(format!(
            "the provider says: op {op} {} — and more",
            ops::NOT_IN_TABLE
        ));
        assert!(
            !GuestCloud::reads_as_predating(op, &quoting),
            "a sentence quoting the words is the guest's refusal"
        );
        assert!(
            GuestCloud::reads_as_predating(
                op,
                &WireCloudError::Refused(format!("op {op} {}", ops::NOT_IN_TABLE))
            ),
            "an old guest's exact SDK sentence"
        );
        assert!(
            !GuestCloud::reads_as_predating(
                ops::LAY_ESTATE,
                &WireCloudError::Refused(format!("op {op} {}", ops::NOT_IN_TABLE))
            ),
            "another op's sentence"
        );
    }
}