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-guest/src/lib.rs

//! **The guest side of a monetize CLOUD** — what a cloud plugin compiles
//! against, and the contract the server's cloud host
//! (`monetize-cloud-wasm`) speaks.
//!
//! Owner ruling 2026-09-25: monetize is BARE. A cloud provider (UpCloud,
//! Scaleway) is a signed `.wasm` the running server takes in, not code linked
//! into its binary. This crate is the half the guest sees:
//!
//! | piece | what it is |
//! |---|---|
//! | [`CloudGuest`] | the trait a cloud plugin implements: its manifest, and ONE [`monetize_cloud::Cloud`] built per call over the host's doors |
//! | [`ops`] | the op table: one number per question the host asks; each is answered from the nine [`monetize_cloud::Cloud`] ops |
//! | [`wire`] | the call envelope (the block's name and settings, then the op's own arguments) and the answer, with `CloudError`'s kind kept |
//! | [`dispatch`] | the op switch — used by the wasm export AND by a native test driving the same guest without a runtime |
//! | [`HostLedger`], [`HostPace`] | the pool ledger kept in the host's durable memory, and the host's clock and wait |
//! | [`export_cloud!`] | the product guest's exports and host bridge (`monetize_product_guest::__export_guest!`), with this dispatch |
//!
//! # What never crosses into the guest
//!
//! * **The spend ceiling.** The host wraps every guest cloud in
//!   `monetize_cloud::cap::SpendCap` exactly as it wraps a linked one; the
//!   guest is never asked what it may spend and never told what is left.
//!   `spend_ceiling`, `reconcile_spend` and `spent_this_month_minor` are not
//!   ops.
//! * **The provider's credential.** The host's HTTP door presents it; a guest
//!   builds requests without an `Authorization` header.
//! * **The growth journal's file.** `grow_set` and `resume_grow` cross, and
//!   every record the provider journals is written THROUGH the host's state door
//!   ([`JOURNAL_KEY`]) to the host's file, fsync'd before the step is taken.
//!   The PURE growth ops (`grow_gate`,
//!   `price_grow`, `survey_set`) do.
//! * **The lay journal's file, the same way** (2026-10-08): `lay_estate` and
//!   `change_plan` cross journaled exactly as a growth does (the job's
//!   `estate-orders/<company>/journal`); `plan_catalog`, `price_estate` and
//!   `set_power` cross plainly. A shop's cloud that is a guest lays a sold
//!   estate with the provider's own code — the shop's credential stays the
//!   host's, presented per request, and the spend ceiling stays the host's.
//!
//! # One provider per call
//!
//! A guest is a fresh instance per call: it remembers nothing, so it builds
//! its provider from the call's settings, answers, and is dropped. What a
//! provider must remember between calls — the pool ledger — lives in the
//! host's durable memory through [`HostLedger`]; the host serialises calls
//! into one cloud, so two calls never interleave on that ledger.

use std::collections::BTreeMap;
use std::time::Duration;

pub use monetize_cloud;
use monetize_cloud::cloud::Priced;
use monetize_cloud::{
    ask, grow, Cloud, CloudError, Ensured, Inventory, Price, Reclaimed, Resource, ResourceId,
    ResourceStatus, Room, Zone,
};
pub use monetize_product_guest as product_guest;
pub use monetize_product_guest::{abi, grants, imports, kinds, Host, Manifest, NoHost};

/// **The host, as a cloud guest holds it**: `'static`, so a provider built for
/// the call can keep it inside its HTTP client rather than only borrow it for
/// one function. The wasm bridge is a `static`; a native test leaks its fake.
pub type HostRef = &'static (dyn Host + Sync);

/// **The op table** — one number per question the host
/// forwards. Numbers are the wire; names are for logs and refusals. Cloud
/// ops are 200..299 (products below 100, vendors 100..199), so no number
/// means two things to two kinds even before the kind gate is asked.
pub mod ops {
    /// `{ provider, zones, ceilings }` — asked once at load, because
    /// `provider()`, `zones()` and `ceilings()` answer borrowed tables the
    /// server reads far more often than it buys.
    pub const ABOUT: u32 = 200;
    /// The start-path proof: a provider that holds a credential proves it here
    /// (UpCloud: one `GET /1.3/price`), so a dead token refuses the block by
    /// name at load and not at the first purchase.
    pub const VERIFY: u32 = 201;
    pub const PRICE: u32 = 210;
    pub const ENSURE: u32 = 211;
    pub const ENSURE_WITH: u32 = 212;
    pub const DESTROY: u32 = 213;
    pub const STATUS: u32 = 214;
    pub const INVENTORY: u32 = 215;
    pub const CAN_SERVE: u32 = 216;
    pub const CEILINGS: u32 = 217;
    pub const ORPHANS_FOR: u32 = 218;
    pub const RELEASE_FOR: u32 = 219;
    /// `Cloud::catalog`: args a `Zone`, answers an
    /// `Option<Price>` (`None`: the provider prices no address of its own). A
    /// guest built before it answers "not in this cloud guest's op table",
    /// which the host reads as `None` (the room price base's fallback).
    pub const PRICE_IPV4: u32 = 220;
    pub const GROW_GATE: u32 = 230;
    pub const PRICE_GROW: u32 = 231;
    pub const SURVEY_SET: u32 = 232;
    /// The JOURNALED growth: the guest runs the provider's own `grow_set` /
    /// `resume_grow`, and every record it journals is written THROUGH the host's
    /// state door under [`super::JOURNAL_KEY`] to the host's journal file (fsync)
    /// before the step is taken. Args: [`super::wire::GrowReq`].
    pub const GROW_SET: u32 = 233;
    pub const RESUME_GROW: u32 = 234;

    // ── laying a whole estate (a shop's sold Gunnar), 2026-10-08 ─────────
    //
    // A guest built before these answers "not in this cloud guest's op
    // table"; the host reads that as the trait's own refusal by name (the
    // cloud cannot lay an estate) — or, for `price_estate`, prices the estate
    // row by row as it always did.

    /// `Priced::Plan`: args an
    /// `EstatePlan`, answers a `Price` (every server, disk at its tier, and
    /// public IPv4 from the provider's own list).
    pub const PRICE_ESTATE: u32 = 240;
    /// `Cloud::catalog`: args a `Zone`,
    /// answers the provider's plans with their monthly price.
    pub const PLAN_CATALOG: u32 = 241;
    /// `Cloud::lay`, JOURNALED like
    /// [`GROW_SET`]: args [`super::wire::LayReq`], answers a `LaidEstate`.
    pub const LAY_ESTATE: u32 = 242;
    /// `Boot::Power`: args
    /// [`super::wire::PowerReq`]. Buys nothing.
    pub const SET_POWER: u32 = 243;
    /// `Change::Plan`, JOURNALED: args
    /// [`super::wire::PlanReq`].
    pub const CHANGE_PLAN: u32 = 244;
    /// `Change::ForgetMedia`, JOURNALED: args
    /// [`super::wire::ForgetReq`], answers the titles given back (2026-10-08).
    pub const FORGET_MEDIA: u32 = 245;
    /// `Boot::Keep`, JOURNALED: args
    /// [`super::wire::KeepBootReq`], answers the `LaidMachine` after (F4, 2026-10-09).
    pub const KEEP_BOOT: u32 = 246;
    /// `Boot::Restore`, JOURNALED: args
    /// [`super::wire::RestoreFromBackupReq`], answers `Option<Restored>` (S8, 2026-10-11).
    pub const RESTORE_FROM_BACKUP: u32 = 247;
    /// `Cloud::firewall`: args the `LaidEstate`, answers
    /// `()` (owner 2026-10-18: every update writes the estate's firewall again first).
    pub const SET_ESTATE_FIREWALL: u32 = 248;
    /// `Change::LabelAdopted`: args the adopted `LaidEstate`, answers
    /// the ids labelled (MAP-SEG 4, 2026-10-26). A guest built before answers NOT_IN_TABLE.
    pub const LABEL_ADOPTED: u32 = 249;

    pub const ALL: &[(u32, &str)] = &[
        (ABOUT, "about"),
        (VERIFY, "verify"),
        (PRICE, "price"),
        (PRICE_IPV4, "price_ipv4"),
        (ENSURE, "ensure"),
        (ENSURE_WITH, "ensure_with"),
        (DESTROY, "destroy"),
        (STATUS, "status"),
        (INVENTORY, "inventory"),
        (CAN_SERVE, "can_serve"),
        (CEILINGS, "ceilings"),
        (ORPHANS_FOR, "orphans_for"),
        (RELEASE_FOR, "release_for"),
        (GROW_GATE, "grow_gate"),
        (PRICE_GROW, "price_grow"),
        (SURVEY_SET, "survey_set"),
        (GROW_SET, "grow_set"),
        (RESUME_GROW, "resume_grow"),
        (PRICE_ESTATE, "price_estate"),
        (PLAN_CATALOG, "plan_catalog"),
        (LAY_ESTATE, "lay_estate"),
        (SET_POWER, "set_power"),
        (CHANGE_PLAN, "change_plan"),
        (FORGET_MEDIA, "forget_media"),
        (KEEP_BOOT, "keep_boot"),
        (RESTORE_FROM_BACKUP, "restore_from_backup"),
        (SET_ESTATE_FIREWALL, "set_estate_firewall"),
        (LABEL_ADOPTED, "label_adopted"),
    ];

    /// The words a guest answers an op it does not have with — what the host
    /// reads as "this guest predates the op".
    pub const NOT_IN_TABLE: &str = "is not in this cloud guest's op table";

    /// The SDK's whole sentence for `op` not in its table — what a guest built
    /// before [`super::wire::WireCloudError::NotInTable`] answers, and the
    /// words the host says a typed `NotInTable` in.
    pub fn not_in_table(op: u32) -> String {
        format!("op {op} {NOT_IN_TABLE}")
    }

    pub fn name(op: u32) -> Option<&'static str> {
        ALL.iter().find(|(n, _)| *n == op).map(|(_, s)| *s)
    }
}

/// **The wire**: JSON both ways, the product guest's encoding.
pub mod wire {
    use super::*;
    pub use monetize_product_guest::wire::{decode, encode};

    /// **Every op's request**: the block the guest is configured by, then the
    /// op's own arguments. The block travels with every call because a guest
    /// is a fresh instance per call and remembers nothing, and because the
    /// guest must never read the config file — the host hands it the keys it
    /// owns (the `[clouds.<name>]` block minus `enabled`, `cap` and every
    /// `guest*` / door key, which are the host's).
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct Call<T> {
        pub name: String,
        pub settings: serde_json::Value,
        pub args: T,
    }

    /// What [`ops::ABOUT`] answers.
    #[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
    pub struct About {
        pub provider: String,
        pub zones: Vec<Zone>,
        pub ceilings: BTreeMap<String, u64>,
    }

    #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
    pub struct EnsureReq {
        pub reference: String,
        pub resource: Resource,
        /// `None` for plain `ensure`.
        pub room: Option<Room>,
    }

    /// `CloudError` across the wire, its kind kept: the transaction releases
    /// on `Refused` and `OverCap`, retries on `Unavailable`, and treats
    /// `UnknownResource` as already given back. `NotInTable(op)`: this guest
    /// has no `op` — the host reads it as "predates `op`" by KIND (stinky
    /// upcloud #21, 5.4); a guest built before it said
    /// [`ops::not_in_table`] as a `Refused`, which the host still reads, exactly.
    #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
    pub enum WireCloudError {
        Refused(String),
        Unavailable(String),
        UnknownResource(ResourceId),
        OverCap(String),
        NotInTable(u32),
        /// Appended (a host before it cannot read it: the host updates first).
        Exhausted(String),
    }

    impl From<CloudError> for WireCloudError {
        fn from(e: CloudError) -> Self {
            match e {
                CloudError::Refused(s) => WireCloudError::Refused(s),
                CloudError::OverCap(s) => WireCloudError::OverCap(s),
                CloudError::Unavailable(s) => WireCloudError::Unavailable(s),
                CloudError::UnknownResource(id) => WireCloudError::UnknownResource(id),
                CloudError::Exhausted(s) => WireCloudError::Exhausted(s),
            }
        }
    }

    impl From<WireCloudError> for CloudError {
        fn from(e: WireCloudError) -> Self {
            match e {
                WireCloudError::Refused(s) => CloudError::Refused(s),
                WireCloudError::OverCap(s) => CloudError::OverCap(s),
                WireCloudError::NotInTable(op) => CloudError::Refused(ops::not_in_table(op)),
                WireCloudError::Unavailable(s) => CloudError::Unavailable(s),
                WireCloudError::UnknownResource(id) => CloudError::UnknownResource(id),
                WireCloudError::Exhausted(s) => CloudError::Exhausted(s),
            }
        }
    }

    /// Every op's answer envelope.
    /// A journaled growth call: the set, and the journal AS THE HOST HOLDS IT now
    /// (the open epoch, the addresses a detach recorded, the target).
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct GrowReq {
        pub set: grow::DataSet,
        pub records: Vec<grow::GrowRecord>,
    }

    /// A journaled LAY: the plan, and the job's journal as the host holds it.
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct LayReq {
        pub plan: monetize_cloud::lay::EstatePlan,
        pub records: Vec<grow::GrowRecord>,
    }

    /// One server stopped (`on = false`) or started.
    #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
    pub struct PowerReq {
        pub server: ResourceId,
        pub on: bool,
    }

    /// A journaled plan change: the server, its new plan, what it adds a
    /// month (the host's ceiling already admitted it), and the journal.
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct PlanReq {
        pub server: ResourceId,
        pub plan: String,
        pub delta: Price,
        pub records: Vec<grow::GrowRecord>,
    }

    /// The release clones of a laid estate given back, and the journal they were recorded in.
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct ForgetReq {
        pub laid: monetize_cloud::lay::LaidEstate,
        pub records: Vec<grow::GrowRecord>,
    }

    /// One laid machine upgraded in place, and the journal it is recorded in.
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct KeepBootReq {
        pub keep: monetize_cloud::lay::KeepBoot,
        pub records: Vec<grow::GrowRecord>,
    }

    /// A sold estate's lost front restored from its backup, and the journal it is recorded in.
    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    pub struct RestoreFromBackupReq {
        pub restore: monetize_cloud::lay::RestoreFromBackup,
        pub records: Vec<grow::GrowRecord>,
    }

    pub type Answer<T> = Result<T, WireCloudError>;
}

use wire::{About, Call, EnsureReq, WireCloudError};

/// **The block a call is made under** — the configured name and the keys the
/// guest owns.
#[derive(Clone, Debug, PartialEq)]
pub struct CallContext {
    pub name: String,
    pub settings: serde_json::Value,
}

/// **A cloud, as a guest writes it.**
pub trait CloudGuest {
    /// `Manifest::of_kind(kinds::CLOUD, "<name>", &[grants…])`: the name is
    /// the `[clouds.<name>]` it must be configured under.
    fn manifest(&self) -> Manifest;

    /// **The provider for ONE call**, built from the block's settings over the
    /// host's doors. It must be cheap: no network call here — the credential
    /// proof is [`CloudGuest::verify`], made once at load.
    fn open(&self, host: HostRef, ctx: &CallContext) -> Result<Box<dyn Cloud>, CloudError>;

    /// **The start-path proof**, asked once when the host loads the module.
    /// The default is that `open` succeeded; a provider with a credential
    /// proves it against the provider (and so refuses a dead token by name at
    /// load, not at the first purchase).
    fn verify(&self, host: HostRef, ctx: &CallContext) -> Result<(), CloudError> {
        self.open(host, ctx).map(|_| ())
    }
}

/// **The op switch** — one implementation, used by the wasm export and by a
/// native test. Answers the JSON bytes of a [`wire::Answer`]; an unknown op or
/// an undecodable request is a `Refused` in that envelope, never a trap.
pub fn dispatch(guest: &dyn CloudGuest, host: HostRef, op: u32, payload: &[u8]) -> Vec<u8> {
    fn ans<T: serde::Serialize>(r: Result<T, CloudError>) -> Vec<u8> {
        wire::encode(&r.map_err(WireCloudError::from))
    }
    fn req<T: serde::de::DeserializeOwned>(
        op: u32,
        payload: &[u8],
    ) -> Result<(CallContext, T), CloudError> {
        let c: Call<T> = wire::decode(payload).map_err(|e| {
            CloudError::Refused(format!(
                "op {op} ({}): undecodable request: {e}",
                ops::name(op).unwrap_or("?")
            ))
        })?;
        Ok((
            CallContext {
                name: c.name,
                settings: c.settings,
            },
            c.args,
        ))
    }
    fn with<A: serde::de::DeserializeOwned, T>(
        guest: &dyn CloudGuest,
        host: HostRef,
        op: u32,
        payload: &[u8],
        f: impl FnOnce(&dyn Cloud, A) -> Result<T, CloudError>,
    ) -> Result<T, CloudError> {
        let (ctx, args) = req::<A>(op, payload)?;
        let p = guest.open(host, &ctx)?;
        f(p.as_ref(), args)
    }
    match op {
        ops::ABOUT => ans(with::<(), _>(guest, host, op, payload, |p, ()| {
            let m = p.manifest();
            Ok(About {
                provider: m.provider,
                zones: m.zones,
                ceilings: ask::ceilings(p),
            })
        })),
        ops::VERIFY => ans(req::<()>(op, payload).and_then(|(ctx, ())| guest.verify(host, &ctx))),
        ops::PRICE => ans(with::<Resource, Price>(guest, host, op, payload, |p, r| {
            p.price(&Priced::Resource(&r))
        })),
        ops::PRICE_IPV4 => ans(with::<Zone, Option<Price>>(
            guest,
            host,
            op,
            payload,
            |p, z| ask::price_ipv4(p, &z),
        )),
        ops::ENSURE | ops::ENSURE_WITH => ans(with::<EnsureReq, Ensured>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::ensure(p, &r.reference, &r.resource, r.room),
        )),
        ops::DESTROY => ans(with::<ResourceId, ()>(guest, host, op, payload, |p, id| {
            ask::destroy_one(p, &id)
        })),
        ops::STATUS => ans(with::<ResourceId, ResourceStatus>(
            guest,
            host,
            op,
            payload,
            |p, id| ask::status(p, &id),
        )),
        ops::INVENTORY => ans(with::<(), Inventory>(guest, host, op, payload, |p, ()| {
            ask::account(p)
        })),
        ops::CAN_SERVE => ans(with::<Vec<Resource>, ()>(
            guest,
            host,
            op,
            payload,
            |p, d| ask::can_serve(p, &d, None).map_err(CloudError::Refused),
        )),
        ops::CEILINGS => ans(with::<(), BTreeMap<String, u64>>(
            guest,
            host,
            op,
            payload,
            |p, ()| Ok(ask::ceilings(p)),
        )),
        ops::ORPHANS_FOR => ans(with::<Vec<String>, Vec<ResourceId>>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::orphans_for(p, &r),
        )),
        ops::RELEASE_FOR => ans(with::<Vec<String>, Reclaimed>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::release_for(p, &r),
        )),
        ops::GROW_GATE => ans(with::<(), ()>(guest, host, op, payload, |p, ()| {
            ask::grow_gate(p)
        })),
        ops::PRICE_GROW => ans(with::<grow::DataSet, Price>(
            guest,
            host,
            op,
            payload,
            |p, s| p.price(&Priced::Grow(&s)),
        )),
        ops::SURVEY_SET => ans(with::<grow::DataSet, grow::SetSurvey>(
            guest,
            host,
            op,
            payload,
            |p, s| ask::survey(p, &s),
        )),
        ops::GROW_SET => ans(with::<wire::GrowReq, grow::GrowOutcome>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::grow(p, &r.set, false, &host_journal(host, r.records)),
        )),
        ops::RESUME_GROW => ans(with::<wire::GrowReq, grow::GrowOutcome>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::settle(p, &r.set, &host_journal(host, r.records)),
        )),
        ops::PRICE_ESTATE => ans(with::<monetize_cloud::lay::EstatePlan, Price>(
            guest,
            host,
            op,
            payload,
            |p, plan| p.price(&Priced::Plan(&plan)),
        )),
        ops::PLAN_CATALOG => ans(with::<Zone, Vec<monetize_cloud::lay::PlanSpec>>(
            guest,
            host,
            op,
            payload,
            |p, z| ask::plan_catalog(p, &z),
        )),
        ops::LAY_ESTATE => ans(with::<wire::LayReq, monetize_cloud::lay::LaidEstate>(
            guest,
            host,
            op,
            payload,
            |p, r| p.lay(&r.plan, &host_journal(host, r.records)),
        )),
        ops::SET_POWER => ans(with::<wire::PowerReq, ()>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::set_power(p, &r.server, r.on),
        )),
        ops::CHANGE_PLAN => ans(with::<wire::PlanReq, ()>(
            guest,
            host,
            op,
            payload,
            |p, r| {
                ask::change_plan(
                    p,
                    &r.server,
                    &r.plan,
                    r.delta,
                    &host_journal(host, r.records),
                )
            },
        )),
        ops::FORGET_MEDIA => ans(with::<wire::ForgetReq, Vec<String>>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::forget_media(p, &r.laid, &host_journal(host, r.records)),
        )),
        ops::KEEP_BOOT => ans(with::<wire::KeepBootReq, monetize_cloud::lay::LaidMachine>(
            guest,
            host,
            op,
            payload,
            |p, r| ask::keep_boot(p, &r.keep, &host_journal(host, r.records)),
        )),
        ops::RESTORE_FROM_BACKUP => ans(with::<
            wire::RestoreFromBackupReq,
            Option<monetize_cloud::lay::Restored>,
        >(guest, host, op, payload, |p, r| {
            ask::restore_from_backup(p, &r.restore, &host_journal(host, r.records))
        })),
        ops::SET_ESTATE_FIREWALL => ans(with::<monetize_cloud::lay::LaidEstate, ()>(
            guest,
            host,
            op,
            payload,
            |p, laid| p.firewall(&laid),
        )),
        ops::LABEL_ADOPTED => ans(with::<monetize_cloud::lay::LaidEstate, Vec<String>>(
            guest,
            host,
            op,
            payload,
            |p, laid| ask::label_adopted(p, &laid),
        )),
        other => wire::encode(&Err::<(), _>(WireCloudError::NotInTable(other))),
    }
}

/// **The pool ledger in the host's durable memory** — `monetize_cloud::pool`'s
/// [`monetize_cloud::pool::LedgerStore`] over the `state` door. The host keeps
/// a cloud's state in a file beside the server's ledger, so a grant written
/// here survives the call, a replacement of the module and a restart.
/// **The state key the host writes THROUGH to its growth journal** while a
/// journaled op runs (`monetize-product-wasm` `Doors::write_through`): one
/// [`grow::GrowRecord`] as JSON per put. Outside such a call the host holds no
/// journal and the put is an ordinary state write nobody reads.
pub const JOURNAL_KEY: &str = "grow-journal";

/// The guest's journal: `records` as the host holds them, every append written
/// through the host's door first ([`JOURNAL_KEY`]).
pub fn host_journal(host: HostRef, records: Vec<grow::GrowRecord>) -> grow::Journal {
    grow::Journal::with_sink(
        records,
        Box::new(move |r: &grow::GrowRecord| {
            let line = serde_json::to_vec(r)
                .map_err(|e| CloudError::Unavailable(format!("grow journal encode: {e}")))?;
            host.state_put(JOURNAL_KEY, &line).map_err(|e| {
                CloudError::Unavailable(format!("grow journal (the host's door): {e}"))
            })
        }),
    )
}

pub struct HostLedger {
    pub host: HostRef,
    pub key: String,
}

impl monetize_cloud::pool::LedgerStore for HostLedger {
    fn load(&self) -> Result<Option<Vec<u8>>, CloudError> {
        self.host.state_get(&self.key).map_err(|e| {
            CloudError::Unavailable(format!("pool ledger (host state `{}`): {e}", self.key))
        })
    }
    fn save(&self, bytes: &[u8]) -> Result<(), CloudError> {
        self.host.state_put(&self.key, bytes).map_err(|e| {
            CloudError::Unavailable(format!("pool ledger (host state `{}`): {e}", self.key))
        })
    }
    fn describe(&self) -> String {
        format!("host state `{}`", self.key)
    }
}

/// **The host's clock and wait** — `monetize_cloud::pace::Pace` over the
/// grant-free `host_clock` and `host_sleep` doors.
pub struct HostPace(pub HostRef);

impl monetize_cloud::pace::Pace for HostPace {
    fn now_ms(&self) -> u64 {
        self.0.clock_unix_ms()
    }
    fn sleep(&self, d: Duration) {
        self.0.sleep_ms(d.as_millis() as u64)
    }
}

/// Generate the exports for a [`CloudGuest`] — the product guest's
/// scaffolding with this crate's [`dispatch`], and `host_sleep` imported.
///
/// ```ignore
/// monetize_cloud_guest::export_cloud!(UpCloudGuest, grants = [http, state]);
/// ```
///
/// The type must implement `CloudGuest` and `Default`. The grant tokens must
/// match what its manifest declares — the host's gate B refuses the module at
/// load if they do not.
#[macro_export]
macro_rules! export_cloud {
    ($guest:ty, grants = [$($grant:ident),* $(,)?] $(,)?) => {
        $crate::product_guest::__export_guest!($guest, $crate::CloudGuest, $crate::dispatch, sleep = yes, grants = [$($grant),*]);
    };
}

#[cfg(test)]
mod tests {
    use super::*;
    use monetize_cloud::{ServerId, Zone};

    /// A cloud with canned answers — what proves the switch keeps each
    /// answer's shape and each error's kind.
    struct Canned;
    struct CannedProvider(String);
    #[allow(dead_code)]
    impl CannedProvider {
        pub fn provider(&self) -> &'static str {
            "canned"
        }
        pub fn zones(&self) -> &[Zone] {
            static Z: std::sync::OnceLock<Vec<Zone>> = std::sync::OnceLock::new();
            Z.get_or_init(|| vec![Zone("se-sto1".into())])
        }
        pub fn price_resource(&self, _: &Resource) -> Result<Price, CloudError> {
            Ok(Price {
                minor_per_month: 230,
                currency: monetize_pay::Currency::Eur,
            })
        }
        pub fn ensure(&self, reference: &str, _: &Resource) -> Result<Ensured, CloudError> {
            if reference == "down" {
                return Err(CloudError::Unavailable("provider off".into()));
            }
            Ok(Ensured::Provisioned {
                id: ResourceId {
                    provider: "canned".into(),
                    id: format!("{}:{reference}", self.0),
                },
                cost: self.price_resource(&Resource::Compute {
                    server: ServerId("s".into()),
                    plan: "p".into(),
                })?,
            })
        }
        pub fn destroy_id(&self, id: &ResourceId) -> Result<(), CloudError> {
            Err(CloudError::UnknownResource(id.clone()))
        }
        pub fn status(&self, _: &ResourceId) -> Result<ResourceStatus, CloudError> {
            Ok(ResourceStatus::Active)
        }
        pub fn list_owned(&self) -> Result<Inventory, CloudError> {
            Err(CloudError::Refused("no account".into()))
        }
    }
    #[allow(unused_variables, unreachable_code)]
    impl monetize_cloud::cloud::Cloud for CannedProvider {
        fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
            monetize_cloud::cloud::CloudManifest {
                provider: self.provider().to_string(),
                zones: self.zones().to_vec(),
                settings: Vec::new(),
                grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()),
            }
        }
        fn catalog(
            &self,
            zone: &monetize_cloud::Zone,
        ) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
            Err(monetize_cloud::lay::unsupported(self.provider()))
        }
        fn price(
            &self,
            what: &monetize_cloud::cloud::Priced<'_>,
        ) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
            match what {
                monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
                monetize_cloud::cloud::Priced::Plan(p) => {
                    monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None)
                }
                monetize_cloud::cloud::Priced::Grow(set) => {
                    Err(monetize_cloud::grow::unsupported(self.provider()))
                }
            }
        }
        fn lay(
            &self,
            plan: &monetize_cloud::lay::EstatePlan,
            journal: &monetize_cloud::grow::Journal,
        ) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
            Err(monetize_cloud::lay::unsupported(self.provider()))
        }
        fn change(
            &self,
            change: &monetize_cloud::cloud::Change,
            journal: &monetize_cloud::grow::Journal,
        ) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
            Ok(match change {
                monetize_cloud::cloud::Change::Plan {
                    server,
                    plan,
                    delta,
                } => {
                    return Err(monetize_cloud::lay::unsupported_verb(
                        self.provider(),
                        "change_plan",
                    ))
                }
                monetize_cloud::cloud::Change::Grow { set, estate } => {
                    return Err(monetize_cloud::grow::unsupported(self.provider()))
                }
                monetize_cloud::cloud::Change::Settle { set } => {
                    return Err(monetize_cloud::grow::unsupported(self.provider()))
                }
                monetize_cloud::cloud::Change::ForgetMedia { laid } => {
                    monetize_cloud::cloud::Changed::Forgotten(Vec::new())
                }
                monetize_cloud::cloud::Change::Ensure {
                    reference,
                    resource,
                    room,
                } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
                monetize_cloud::cloud::Change::Retire { reference, machine } => {
                    monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(
                        self, reference, machine,
                    )?)
                }
                monetize_cloud::cloud::Change::LabelAdopted { .. } => {
                    return Err(monetize_cloud::lay::unsupported_verb(
                        self.provider(),
                        "label_adopted",
                    ))
                }
            })
        }
        fn boot(
            &self,
            boot: &monetize_cloud::cloud::Boot,
            journal: &monetize_cloud::grow::Journal,
        ) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
            Ok(match boot {
                monetize_cloud::cloud::Boot::Keep(k) => {
                    return Err(monetize_cloud::lay::unsupported_verb(
                        self.provider(),
                        "keep_boot",
                    ))
                }
                monetize_cloud::cloud::Boot::Power { server, on } => {
                    return Err(monetize_cloud::lay::unsupported_verb(
                        self.provider(),
                        "set_power",
                    ))
                }
                monetize_cloud::cloud::Boot::Restore(r) => {
                    return Err(monetize_cloud::lay::unsupported_verb(
                        self.provider(),
                        "restore_from_backup",
                    ))
                }
            })
        }
        fn firewall(
            &self,
            laid: &monetize_cloud::lay::LaidEstate,
        ) -> Result<(), monetize_cloud::CloudError> {
            Err(monetize_cloud::lay::unsupported_verb(
                self.provider(),
                "set_estate_firewall",
            ))
        }
        fn destroy(
            &self,
            what: &monetize_cloud::cloud::Give<'_>,
        ) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
            match what {
                monetize_cloud::cloud::Give::References(refs) => Ok(
                    monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| {
                        self.destroy_id(id)
                    }),
                ),
                monetize_cloud::cloud::Give::Resource(id) => {
                    self.destroy_id(id)?;
                    Ok(monetize_cloud::Reclaimed {
                        destroyed: vec![(*id).clone()],
                        ..monetize_cloud::Reclaimed::default()
                    })
                }
            }
        }
        fn inventory(
            &self,
            what: &monetize_cloud::cloud::Listing<'_>,
        ) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
            Ok(match what {
                monetize_cloud::cloud::Listing::Account => {
                    monetize_cloud::cloud::Listed::Account(self.list_owned()?)
                }
                monetize_cloud::cloud::Listing::Set(set) => {
                    return Err(monetize_cloud::grow::unsupported(self.provider()))
                }
                monetize_cloud::cloud::Listing::Status(id) => {
                    monetize_cloud::cloud::Listed::Status(self.status(id)?)
                }
                monetize_cloud::cloud::Listing::Under(refs) => {
                    monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(
                        self.list_owned()?,
                        refs,
                    ))
                }
                monetize_cloud::cloud::Listing::Room { delta, room } => {
                    monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft {
                        serve: Ok(()),
                        left_bytes: None,
                        ceilings: Default::default(),
                    })
                }
                monetize_cloud::cloud::Listing::Admits(_plan) => {
                    monetize_cloud::cloud::Listed::Admitted
                }
            })
        }
    }
    impl CloudGuest for Canned {
        fn manifest(&self) -> Manifest {
            Manifest::of_kind(kinds::CLOUD, "canned", &[])
        }
        fn open(&self, _: HostRef, ctx: &CallContext) -> Result<Box<dyn Cloud>, CloudError> {
            Ok(Box::new(CannedProvider(
                ctx.settings["zone"].as_str().unwrap_or("none").to_string(),
            )))
        }
    }

    fn call<T: serde::Serialize>(args: T) -> Vec<u8> {
        wire::encode(&Call {
            name: "canned".into(),
            settings: serde_json::json!({"zone": "se-sto1"}),
            args,
        })
    }

    #[test]
    fn every_op_round_trips_and_errors_keep_their_kind() {
        let host: HostRef = &NoHost;
        let about: wire::Answer<About> =
            wire::decode(&dispatch(&Canned, host, ops::ABOUT, &call(()))).unwrap();
        assert_eq!(about.unwrap().zones, vec![Zone("se-sto1".into())]);
        let disk = Resource::Disk {
            server: ServerId("s".into()),
            gib: 10,
        };
        let ok: wire::Answer<Ensured> = wire::decode(&dispatch(
            &Canned,
            host,
            ops::ENSURE,
            &call(EnsureReq {
                reference: "r1".into(),
                resource: disk.clone(),
                room: None,
            }),
        ))
        .unwrap();
        match ok.unwrap() {
            Ensured::Provisioned { id, cost } => {
                assert_eq!(id.id, "se-sto1:r1", "the settings reached the provider");
                assert_eq!(cost.minor_per_month, 230);
            }
            other => panic!("{other:?}"),
        }
        let down: wire::Answer<Ensured> = wire::decode(&dispatch(
            &Canned,
            host,
            ops::ENSURE_WITH,
            &call(EnsureReq {
                reference: "down".into(),
                resource: disk,
                room: Some(Room::Buy),
            }),
        ))
        .unwrap();
        assert!(
            matches!(CloudError::from(down.unwrap_err()), CloudError::Unavailable(m) if m == "provider off")
        );
        let gone: wire::Answer<()> = wire::decode(&dispatch(
            &Canned,
            host,
            ops::DESTROY,
            &call(ResourceId {
                provider: "canned".into(),
                id: "x".into(),
            }),
        ))
        .unwrap();
        assert!(matches!(gone.unwrap_err(), WireCloudError::UnknownResource(id) if id.id == "x"));
        let refused: wire::Answer<Inventory> =
            wire::decode(&dispatch(&Canned, host, ops::INVENTORY, &call(()))).unwrap();
        assert!(matches!(refused.unwrap_err(), WireCloudError::Refused(m) if m == "no account"));
        let grow: wire::Answer<()> =
            wire::decode(&dispatch(&Canned, host, ops::GROW_GATE, &call(()))).unwrap();
        assert!(
            matches!(grow.unwrap_err(), WireCloudError::Refused(m) if m.contains("canned")),
            "the default growth refusal crosses by name"
        );
        let unknown: wire::Answer<()> =
            wire::decode(&dispatch(&Canned, host, 999, &call(()))).unwrap();
        assert_eq!(
            unknown.unwrap_err(),
            WireCloudError::NotInTable(999),
            "an op not in the table is said by KIND"
        );
        let bad: wire::Answer<Price> =
            wire::decode(&dispatch(&Canned, host, ops::PRICE, b"not json")).unwrap();
        assert!(
            matches!(bad.unwrap_err(), WireCloudError::Refused(m) if m.contains("undecodable"))
        );
    }

    #[test]
    fn the_manifest_is_a_cloud_and_the_ops_are_named() {
        let m = Canned.manifest();
        assert_eq!(
            (m.kind.as_str(), m.product.as_str()),
            (kinds::CLOUD, "canned")
        );
        for (n, name) in ops::ALL {
            assert_eq!(ops::name(*n), Some(*name));
        }
        // An older manifest (no `kind`) reads as a product.
        let old: Manifest = wire::decode(
            br#"{"abi_min":1,"abi_max":1,"product":"x","grants":[],"identity":false}"#,
        )
        .unwrap();
        assert_eq!(old.kind, kinds::PRODUCT);
    }

    /// **GROW_SET crosses**: the provider's `grow_set` runs in the guest against the
    /// records the host sent, and every record it journals is a `state_put` of
    /// [`JOURNAL_KEY`] — in order, before the answer.
    #[test]
    fn grow_set_runs_in_the_guest_and_journals_through_the_host_door() {
        struct Journaling;
        #[allow(dead_code)]
        impl Journaling {
            pub fn provider(&self) -> &'static str {
                "canned"
            }
            pub fn zones(&self) -> &[Zone] {
                &[]
            }
            pub fn price_resource(&self, _: &Resource) -> Result<Price, CloudError> {
                Err(CloudError::Refused("x".into()))
            }
            pub fn ensure(&self, _: &str, _: &Resource) -> Result<Ensured, CloudError> {
                Err(CloudError::Refused("x".into()))
            }
            pub fn destroy_id(&self, _: &ResourceId) -> Result<(), CloudError> {
                Ok(())
            }
            pub fn status(&self, _: &ResourceId) -> Result<ResourceStatus, CloudError> {
                Ok(ResourceStatus::Active)
            }
            pub fn list_owned(&self) -> Result<Inventory, CloudError> {
                Err(CloudError::Refused("x".into()))
            }
            pub fn grow_set(
                &self,
                set: &grow::DataSet,
                journal: &grow::Journal,
            ) -> Result<grow::GrowOutcome, CloudError> {
                assert_eq!(journal.open_epoch(), Some(5), "the host's records arrived");
                journal.append(grow::GrowRecord::set_wide(
                    10,
                    5,
                    grow::step::RESUME,
                    grow::Mark::Intent,
                    set.product.clone(),
                ))?;
                journal.append(grow::GrowRecord::set_wide(
                    11,
                    5,
                    grow::step::CLOSED,
                    grow::Mark::Confirmed,
                    "untouched",
                ))?;
                Ok(grow::GrowOutcome::Untouched {
                    why: "nothing to do".into(),
                })
            }
        }
        #[allow(unused_variables, unreachable_code)]
        impl monetize_cloud::cloud::Cloud for Journaling {
            fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
                monetize_cloud::cloud::CloudManifest {
                    provider: self.provider().to_string(),
                    zones: self.zones().to_vec(),
                    settings: Vec::new(),
                    grow_refused: Some(
                        monetize_cloud::grow::unsupported(self.provider()).to_string(),
                    ),
                }
            }
            fn catalog(
                &self,
                zone: &monetize_cloud::Zone,
            ) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
                Err(monetize_cloud::lay::unsupported(self.provider()))
            }
            fn price(
                &self,
                what: &monetize_cloud::cloud::Priced<'_>,
            ) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
                match what {
                    monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
                    monetize_cloud::cloud::Priced::Plan(p) => {
                        monetize_cloud::price_estate_by_rows(p, |r| self.price_resource(r), None)
                    }
                    monetize_cloud::cloud::Priced::Grow(set) => {
                        Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                }
            }
            fn lay(
                &self,
                plan: &monetize_cloud::lay::EstatePlan,
                journal: &monetize_cloud::grow::Journal,
            ) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
                Err(monetize_cloud::lay::unsupported(self.provider()))
            }
            fn change(
                &self,
                change: &monetize_cloud::cloud::Change,
                journal: &monetize_cloud::grow::Journal,
            ) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
                Ok(match change {
                    monetize_cloud::cloud::Change::Plan {
                        server,
                        plan,
                        delta,
                    } => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "change_plan",
                        ))
                    }
                    monetize_cloud::cloud::Change::Grow { set, estate } => {
                        monetize_cloud::cloud::Changed::Grown(self.grow_set(set, journal)?)
                    }
                    monetize_cloud::cloud::Change::Settle { set } => {
                        return Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                    monetize_cloud::cloud::Change::ForgetMedia { laid } => {
                        monetize_cloud::cloud::Changed::Forgotten(Vec::new())
                    }
                    monetize_cloud::cloud::Change::Ensure {
                        reference,
                        resource,
                        room,
                    } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
                    monetize_cloud::cloud::Change::Retire { reference, machine } => {
                        monetize_cloud::cloud::Changed::Retired(
                            monetize_cloud::cloud::retire_by_rows(self, reference, machine)?,
                        )
                    }
                    monetize_cloud::cloud::Change::LabelAdopted { .. } => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "label_adopted",
                        ))
                    }
                })
            }
            fn boot(
                &self,
                boot: &monetize_cloud::cloud::Boot,
                journal: &monetize_cloud::grow::Journal,
            ) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
                Ok(match boot {
                    monetize_cloud::cloud::Boot::Keep(k) => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "keep_boot",
                        ))
                    }
                    monetize_cloud::cloud::Boot::Power { server, on } => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "set_power",
                        ))
                    }
                    monetize_cloud::cloud::Boot::Restore(r) => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "restore_from_backup",
                        ))
                    }
                })
            }
            fn firewall(
                &self,
                laid: &monetize_cloud::lay::LaidEstate,
            ) -> Result<(), monetize_cloud::CloudError> {
                Err(monetize_cloud::lay::unsupported_verb(
                    self.provider(),
                    "set_estate_firewall",
                ))
            }
            fn destroy(
                &self,
                what: &monetize_cloud::cloud::Give<'_>,
            ) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
                match what {
                    monetize_cloud::cloud::Give::References(refs) => Ok(
                        monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| {
                            self.destroy_id(id)
                        }),
                    ),
                    monetize_cloud::cloud::Give::Resource(id) => {
                        self.destroy_id(id)?;
                        Ok(monetize_cloud::Reclaimed {
                            destroyed: vec![(*id).clone()],
                            ..monetize_cloud::Reclaimed::default()
                        })
                    }
                }
            }
            fn inventory(
                &self,
                what: &monetize_cloud::cloud::Listing<'_>,
            ) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
                Ok(match what {
                    monetize_cloud::cloud::Listing::Account => {
                        monetize_cloud::cloud::Listed::Account(self.list_owned()?)
                    }
                    monetize_cloud::cloud::Listing::Set(set) => {
                        return Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                    monetize_cloud::cloud::Listing::Status(id) => {
                        monetize_cloud::cloud::Listed::Status(self.status(id)?)
                    }
                    monetize_cloud::cloud::Listing::Under(refs) => {
                        monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(
                            self.list_owned()?,
                            refs,
                        ))
                    }
                    monetize_cloud::cloud::Listing::Room { delta, room } => {
                        monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft {
                            serve: Ok(()),
                            left_bytes: None,
                            ceilings: Default::default(),
                        })
                    }
                    monetize_cloud::cloud::Listing::Admits(_plan) => {
                        monetize_cloud::cloud::Listed::Admitted
                    }
                })
            }
        }
        struct G;
        impl CloudGuest for G {
            fn manifest(&self) -> Manifest {
                Manifest::of_kind(kinds::CLOUD, "canned", &[])
            }
            fn open(&self, _: HostRef, _: &CallContext) -> Result<Box<dyn Cloud>, CloudError> {
                Ok(Box::new(Journaling))
            }
        }
        struct Puts(std::sync::Mutex<Vec<(String, Vec<u8>)>>);
        impl Host for Puts {
            fn state_put(&self, key: &str, value: &[u8]) -> Result<(), String> {
                self.0.lock().unwrap().push((key.into(), value.to_vec()));
                Ok(())
            }
        }
        let puts: &'static Puts = Box::leak(Box::new(Puts(Default::default())));
        let set = grow::DataSet {
            product: "gunnar".into(),
            server: monetize_cloud::ServerId("s".into()),
            members: vec![],
            target_gib: 20,
        };
        let open =
            grow::GrowRecord::set_wide(1, 5, grow::step::PREFLIGHT, grow::Mark::Intent, "open")
                .with_gib(20);
        let out: wire::Answer<grow::GrowOutcome> = wire::decode(&dispatch(
            &G,
            puts,
            ops::GROW_SET,
            &call(wire::GrowReq {
                set,
                records: vec![open],
            }),
        ))
        .unwrap();
        assert!(
            matches!(out.unwrap(), grow::GrowOutcome::Untouched { ref why } if why == "nothing to do")
        );
        let got = puts.0.lock().unwrap();
        assert_eq!(got.len(), 2);
        assert!(got.iter().all(|(k, _)| k == JOURNAL_KEY));
        let recs: Vec<grow::GrowRecord> = got
            .iter()
            .map(|(_, v)| serde_json::from_slice(v).unwrap())
            .collect();
        assert_eq!(
            (recs[0].step.as_str(), recs[1].step.as_str()),
            (grow::step::RESUME, grow::step::CLOSED)
        );
        assert_eq!(recs[0].detail, "gunnar", "the set reached the provider");
    }

    /// ★ **The estate ops cross** (2026-10-08, test2's shop: "the upcloud
    /// cloud cannot lay an estate"): the plan list, the estate's price, the
    /// lay — journaled through the host's door like a growth, in order, before
    /// the answer — the power switch and the journaled plan change. A provider
    /// with no estate verbs answers the trait's own refusal, by name.
    #[test]
    fn the_estate_ops_cross_and_the_lay_journals_through_the_host_door() {
        use monetize_cloud::lay::{self, EstatePlan, LaidEstate, LaidMachine, PlanSpec};
        struct Laying;
        #[allow(dead_code)]
        impl Laying {
            pub fn provider(&self) -> &'static str {
                "canned"
            }
            pub fn zones(&self) -> &[Zone] {
                &[]
            }
            pub fn price_resource(&self, _: &Resource) -> Result<Price, CloudError> {
                Err(CloudError::Refused("x".into()))
            }
            pub fn ensure(&self, _: &str, _: &Resource) -> Result<Ensured, CloudError> {
                Err(CloudError::Refused("x".into()))
            }
            pub fn destroy_id(&self, _: &ResourceId) -> Result<(), CloudError> {
                Ok(())
            }
            pub fn status(&self, _: &ResourceId) -> Result<ResourceStatus, CloudError> {
                Ok(ResourceStatus::Active)
            }
            pub fn list_owned(&self) -> Result<Inventory, CloudError> {
                Err(CloudError::Refused("x".into()))
            }
            pub fn plan_catalog(&self, zone: &Zone) -> Result<Vec<PlanSpec>, CloudError> {
                Ok(vec![PlanSpec {
                    name: format!("1x2@{}", zone.0),
                    cores: 1,
                    memory_mib: 2048,
                    price: Price {
                        minor_per_month: 700,
                        currency: monetize_pay::Currency::Eur,
                    },
                }])
            }
            pub fn price_estate(&self, plan: &EstatePlan) -> Result<Price, CloudError> {
                Ok(Price {
                    minor_per_month: 100 * plan.machines.len() as u64,
                    currency: monetize_pay::Currency::Eur,
                })
            }
            pub fn lay_estate(
                &self,
                plan: &EstatePlan,
                journal: &grow::Journal,
            ) -> Result<LaidEstate, CloudError> {
                assert_eq!(journal.records().len(), 1, "the host's records arrived");
                journal.append(grow::GrowRecord::set_wide(
                    10,
                    9,
                    lay::step::LAY,
                    grow::Mark::Intent,
                    plan.reference.clone(),
                ))?;
                journal.append(grow::GrowRecord::set_wide(
                    11,
                    9,
                    grow::step::CLOSED,
                    grow::Mark::Confirmed,
                    "laid",
                ))?;
                Ok(LaidEstate {
                    reference: plan.reference.clone(),
                    company: plan.company.clone(),
                    zone: plan.zone.clone(),
                    machines: vec![LaidMachine {
                        role: "front".into(),
                        hostname: "f".into(),
                        title: "f".into(),
                        plan: "1x2".into(),
                        server: "uuid-f".into(),
                        public_ipv4: "192.0.2.1".into(),
                        utility_ipv4: "10.0.0.1".into(),
                        disks: vec![],
                        medium_clone: String::new(),
                    }],
                    narrate_from: plan.narrate_from.clone(),
                    shape: plan.shape.clone(),
                })
            }
            pub fn set_power(&self, server: &ResourceId, on: bool) -> Result<(), CloudError> {
                if on {
                    Ok(())
                } else {
                    Err(CloudError::UnknownResource(server.clone()))
                }
            }
            pub fn change_plan(
                &self,
                server: &ResourceId,
                plan: &str,
                delta: Price,
                journal: &grow::Journal,
            ) -> Result<(), CloudError> {
                journal.append(grow::GrowRecord::set_wide(
                    12,
                    9,
                    lay::step::PLAN,
                    grow::Mark::Confirmed,
                    format!("{}:{plan}:{}", server.id, delta.minor_per_month),
                ))
            }
        }
        #[allow(unused_variables, unreachable_code)]
        impl monetize_cloud::cloud::Cloud for Laying {
            fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
                monetize_cloud::cloud::CloudManifest {
                    provider: self.provider().to_string(),
                    zones: self.zones().to_vec(),
                    settings: Vec::new(),
                    grow_refused: Some(
                        monetize_cloud::grow::unsupported(self.provider()).to_string(),
                    ),
                }
            }
            fn catalog(
                &self,
                zone: &monetize_cloud::Zone,
            ) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
                Ok(monetize_cloud::cloud::Catalog {
                    plans: self.plan_catalog(zone)?,
                    ipv4: None,
                })
            }
            fn price(
                &self,
                what: &monetize_cloud::cloud::Priced<'_>,
            ) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
                match what {
                    monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
                    monetize_cloud::cloud::Priced::Plan(p) => self.price_estate(p),
                    monetize_cloud::cloud::Priced::Grow(set) => {
                        Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                }
            }
            fn lay(
                &self,
                plan: &monetize_cloud::lay::EstatePlan,
                journal: &monetize_cloud::grow::Journal,
            ) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
                self.lay_estate(plan, journal)
            }
            fn change(
                &self,
                change: &monetize_cloud::cloud::Change,
                journal: &monetize_cloud::grow::Journal,
            ) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
                Ok(match change {
                    monetize_cloud::cloud::Change::Plan {
                        server,
                        plan,
                        delta,
                    } => {
                        self.change_plan(server, plan, *delta, journal)?;
                        monetize_cloud::cloud::Changed::Done
                    }
                    monetize_cloud::cloud::Change::Grow { set, estate } => {
                        return Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                    monetize_cloud::cloud::Change::Settle { set } => {
                        return Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                    monetize_cloud::cloud::Change::ForgetMedia { laid } => {
                        monetize_cloud::cloud::Changed::Forgotten(Vec::new())
                    }
                    monetize_cloud::cloud::Change::Ensure {
                        reference,
                        resource,
                        room,
                    } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
                    monetize_cloud::cloud::Change::Retire { reference, machine } => {
                        monetize_cloud::cloud::Changed::Retired(
                            monetize_cloud::cloud::retire_by_rows(self, reference, machine)?,
                        )
                    }
                    monetize_cloud::cloud::Change::LabelAdopted { .. } => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "label_adopted",
                        ))
                    }
                })
            }
            fn boot(
                &self,
                boot: &monetize_cloud::cloud::Boot,
                journal: &monetize_cloud::grow::Journal,
            ) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
                Ok(match boot {
                    monetize_cloud::cloud::Boot::Keep(k) => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "keep_boot",
                        ))
                    }
                    monetize_cloud::cloud::Boot::Power { server, on } => {
                        self.set_power(server, *on)?;
                        monetize_cloud::cloud::Booted::Powered
                    }
                    monetize_cloud::cloud::Boot::Restore(r) => {
                        return Err(monetize_cloud::lay::unsupported_verb(
                            self.provider(),
                            "restore_from_backup",
                        ))
                    }
                })
            }
            fn firewall(
                &self,
                laid: &monetize_cloud::lay::LaidEstate,
            ) -> Result<(), monetize_cloud::CloudError> {
                Err(monetize_cloud::lay::unsupported_verb(
                    self.provider(),
                    "set_estate_firewall",
                ))
            }
            fn destroy(
                &self,
                what: &monetize_cloud::cloud::Give<'_>,
            ) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
                match what {
                    monetize_cloud::cloud::Give::References(refs) => Ok(
                        monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| {
                            self.destroy_id(id)
                        }),
                    ),
                    monetize_cloud::cloud::Give::Resource(id) => {
                        self.destroy_id(id)?;
                        Ok(monetize_cloud::Reclaimed {
                            destroyed: vec![(*id).clone()],
                            ..monetize_cloud::Reclaimed::default()
                        })
                    }
                }
            }
            fn inventory(
                &self,
                what: &monetize_cloud::cloud::Listing<'_>,
            ) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
                Ok(match what {
                    monetize_cloud::cloud::Listing::Account => {
                        monetize_cloud::cloud::Listed::Account(self.list_owned()?)
                    }
                    monetize_cloud::cloud::Listing::Set(set) => {
                        return Err(monetize_cloud::grow::unsupported(self.provider()))
                    }
                    monetize_cloud::cloud::Listing::Status(id) => {
                        monetize_cloud::cloud::Listed::Status(self.status(id)?)
                    }
                    monetize_cloud::cloud::Listing::Under(refs) => {
                        monetize_cloud::cloud::Listed::Under(monetize_cloud::cloud::under(
                            self.list_owned()?,
                            refs,
                        ))
                    }
                    monetize_cloud::cloud::Listing::Room { delta, room } => {
                        monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft {
                            serve: Ok(()),
                            left_bytes: None,
                            ceilings: Default::default(),
                        })
                    }
                    monetize_cloud::cloud::Listing::Admits(_plan) => {
                        monetize_cloud::cloud::Listed::Admitted
                    }
                })
            }
        }
        struct G;
        impl CloudGuest for G {
            fn manifest(&self) -> Manifest {
                Manifest::of_kind(kinds::CLOUD, "canned", &[])
            }
            fn open(&self, _: HostRef, _: &CallContext) -> Result<Box<dyn Cloud>, CloudError> {
                Ok(Box::new(Laying))
            }
        }
        struct Puts(std::sync::Mutex<Vec<(String, Vec<u8>)>>);
        impl Host for Puts {
            fn state_put(&self, key: &str, value: &[u8]) -> Result<(), String> {
                self.0.lock().unwrap().push((key.into(), value.to_vec()));
                Ok(())
            }
        }
        let puts: &'static Puts = Box::leak(Box::new(Puts(Default::default())));
        let plan = EstatePlan {
            reference: lay::reference_of("acme"),
            company: "acme".into(),
            zone: Zone("se-sto1".into()),
            machines: vec![],
            narrate_from: String::new(),
            shape: None,
        };

        let plans: wire::Answer<Vec<PlanSpec>> = wire::decode(&dispatch(
            &G,
            puts,
            ops::PLAN_CATALOG,
            &call(Zone("se-sto1".into())),
        ))
        .unwrap();
        assert_eq!(
            plans.unwrap()[0].name,
            "1x2@se-sto1",
            "the zone reached the provider"
        );
        let price: wire::Answer<Price> =
            wire::decode(&dispatch(&G, puts, ops::PRICE_ESTATE, &call(plan.clone()))).unwrap();
        assert_eq!(
            price.unwrap().minor_per_month,
            0,
            "the provider's own estate price, not a sum the host made"
        );

        let open = grow::GrowRecord::set_wide(1, 9, lay::step::LAY, grow::Mark::Intent, "earlier");
        let laid: wire::Answer<LaidEstate> = wire::decode(&dispatch(
            &G,
            puts,
            ops::LAY_ESTATE,
            &call(wire::LayReq {
                plan: plan.clone(),
                records: vec![open.clone()],
            }),
        ))
        .unwrap();
        let laid = laid.unwrap();
        assert_eq!(
            (laid.reference.as_str(), laid.machines[0].server.as_str()),
            ("estate/acme", "uuid-f")
        );
        {
            let got = puts.0.lock().unwrap();
            assert_eq!(
                got.len(),
                2,
                "both lay records were written through the host's door"
            );
            assert!(got.iter().all(|(k, _)| k == JOURNAL_KEY));
            let recs: Vec<grow::GrowRecord> = got
                .iter()
                .map(|(_, v)| serde_json::from_slice(v).unwrap())
                .collect();
            assert_eq!(
                (recs[0].step.as_str(), recs[1].step.as_str()),
                (lay::step::LAY, grow::step::CLOSED)
            );
            assert_eq!(recs[0].detail, "estate/acme");
        }

        let srv = ResourceId {
            provider: "canned".into(),
            id: "uuid-m".into(),
        };
        let on: wire::Answer<()> = wire::decode(&dispatch(
            &G,
            puts,
            ops::SET_POWER,
            &call(wire::PowerReq {
                server: srv.clone(),
                on: true,
            }),
        ))
        .unwrap();
        assert!(on.is_ok());
        let off: wire::Answer<()> = wire::decode(&dispatch(
            &G,
            puts,
            ops::SET_POWER,
            &call(wire::PowerReq {
                server: srv.clone(),
                on: false,
            }),
        ))
        .unwrap();
        assert!(
            matches!(off.unwrap_err(), WireCloudError::UnknownResource(id) if id.id == "uuid-m"),
            "the error's kind is kept"
        );
        let delta = Price {
            minor_per_month: 1234,
            currency: monetize_pay::Currency::Eur,
        };
        let changed: wire::Answer<()> = wire::decode(&dispatch(
            &G,
            puts,
            ops::CHANGE_PLAN,
            &call(wire::PlanReq {
                server: srv,
                plan: "2x4".into(),
                delta,
                records: vec![],
            }),
        ))
        .unwrap();
        assert!(changed.is_ok());
        let last: grow::GrowRecord =
            serde_json::from_slice(&puts.0.lock().unwrap().last().unwrap().1).unwrap();
        assert_eq!(
            (last.step.as_str(), last.detail.as_str()),
            (lay::step::PLAN, "uuid-m:2x4:1234")
        );

        // A provider with no estate verbs: the trait's own refusal, by name.
        let none: wire::Answer<Vec<PlanSpec>> = wire::decode(&dispatch(
            &Canned,
            puts,
            ops::PLAN_CATALOG,
            &call(Zone("se-sto1".into())),
        ))
        .unwrap();
        assert!(
            matches!(none.unwrap_err(), WireCloudError::Refused(m) if m.contains("canned cloud cannot lay an estate"))
        );
        let none: wire::Answer<LaidEstate> = wire::decode(&dispatch(
            &Canned,
            puts,
            ops::LAY_ESTATE,
            &call(wire::LayReq {
                plan,
                records: vec![],
            }),
        ))
        .unwrap();
        assert!(
            matches!(none.unwrap_err(), WireCloudError::Refused(m) if m.contains("cannot lay an estate"))
        );
    }

    /// The pool ledger through the host's memory: a grant written in one call
    /// is read by the next, which is what makes `ensure` idempotent in a guest.
    #[test]
    fn the_ledger_lives_in_the_host_state_between_calls() {
        struct Mem(std::sync::Mutex<BTreeMap<String, Vec<u8>>>);
        impl Host for Mem {
            fn state_get(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
                Ok(self.0.lock().unwrap().get(key).cloned())
            }
            fn state_put(&self, key: &str, value: &[u8]) -> Result<(), String> {
                self.0.lock().unwrap().insert(key.into(), value.to_vec());
                Ok(())
            }
        }
        let host: HostRef = Box::leak(Box::new(Mem(Default::default())));
        let store = || {
            Box::new(HostLedger {
                host,
                key: "pool".into(),
            })
        };
        let mut first = monetize_cloud::pool::Ledger::with_store(store()).unwrap();
        first
            .record(
                "r|disk:s:10",
                monetize_cloud::pool::Grant::OnHand {
                    server: "s".into(),
                    gib: 10,
                    free_after: 0,
                },
            )
            .unwrap();
        let second = monetize_cloud::pool::Ledger::with_store(store()).unwrap();
        assert_eq!(second.sold("s"), 10);
        assert!(second.grant("r|disk:s:10").is_some());
    }

    /// MON-OSSF (2026-10-31): **every kind of [`CloudError`] crosses the wire and back as
    /// itself**, since the transaction acts on the kind (release on Refused and OverCap, retry on
    /// Unavailable, already-gone on UnknownResource, a full pool on Exhausted); `NotInTable` comes
    /// back as the exact words a guest built before it said, which the host still reads.
    #[test]
    fn every_cloud_error_kind_round_trips_and_not_in_table_reads_as_the_old_words() {
        let id = ResourceId {
            provider: "canned".into(),
            id: "x".into(),
        };
        for e in [
            CloudError::Refused("r".into()),
            CloudError::Unavailable("u".into()),
            CloudError::UnknownResource(id),
            CloudError::OverCap("o".into()),
            CloudError::Exhausted("e".into()),
        ] {
            let words = format!("{e:?}");
            let w = WireCloudError::from(e);
            let back: WireCloudError = wire::decode(&wire::encode(&w)).unwrap();
            assert_eq!(back, w);
            assert_eq!(
                format!("{:?}", CloudError::from(back)),
                words,
                "the kind and its payload survive"
            );
        }
        assert_eq!(
            ops::not_in_table(7),
            "op 7 is not in this cloud guest's op table"
        );
        match CloudError::from(WireCloudError::NotInTable(7)) {
            CloudError::Refused(s) => assert_eq!(s, ops::not_in_table(7)),
            other => panic!("{other:?}"),
        }
    }

    /// Every op in the table is switched: undecodable bytes are refused under the op's own
    /// name, never as `NotInTable` (the host reads that as "this guest predates the op").
    #[test]
    fn every_op_in_the_table_is_switched_and_refuses_undecodable_bytes_by_its_name() {
        for (op, name) in ops::ALL.iter().filter(|(op, _)| *op != ops::ABOUT) {
            let a: wire::Answer<serde_json::Value> =
                wire::decode(&dispatch(&Canned, &NoHost, *op, b"\xff")).unwrap();
            match a {
                Err(WireCloudError::Refused(m)) => assert!(
                    m.contains("undecodable request") && m.contains(&format!("({name})")),
                    "{name}: {m}"
                ),
                other => panic!("{name}: {other:?}"),
            }
        }
        let mut numbers: Vec<u32> = ops::ALL.iter().map(|(n, _)| *n).collect();
        numbers.sort();
        numbers.dedup();
        assert_eq!(numbers.len(), ops::ALL.len(), "no op number is used twice");
    }

    fn laid_machine() -> monetize_cloud::lay::LaidMachine {
        monetize_cloud::lay::LaidMachine {
            role: "main".into(),
            hostname: "main.acme".into(),
            title: "acme main".into(),
            plan: "1x2".into(),
            server: "uuid-m".into(),
            public_ipv4: "192.0.2.10".into(),
            utility_ipv4: "10.0.0.10".into(),
            disks: vec![],
            medium_clone: String::new(),
        }
    }

    fn laid() -> monetize_cloud::lay::LaidEstate {
        monetize_cloud::lay::LaidEstate {
            reference: "estate/acme".into(),
            company: "acme".into(),
            zone: Zone("se-sto1".into()),
            machines: vec![laid_machine()],
            narrate_from: String::new(),
            shape: None,
        }
    }

    /// A provider without the estate verbs answers each of them through the switch with ITS
    /// refusal by name (the verb named), and the read-only ops with what it holds.
    #[test]
    fn a_provider_without_the_estate_verbs_refuses_each_by_name_through_the_switch() {
        let host: HostRef = &NoHost;
        let verify: wire::Answer<()> =
            wire::decode(&dispatch(&Canned, host, ops::VERIFY, &call(()))).unwrap();
        assert_eq!(
            verify,
            Ok(()),
            "the default proof is that the provider opens"
        );
        let id = ResourceId {
            provider: "canned".into(),
            id: "x".into(),
        };
        let st: wire::Answer<ResourceStatus> =
            wire::decode(&dispatch(&Canned, host, ops::STATUS, &call(id))).unwrap();
        assert_eq!(st, Ok(ResourceStatus::Active));
        let ceilings: wire::Answer<BTreeMap<String, u64>> =
            wire::decode(&dispatch(&Canned, host, ops::CEILINGS, &call(()))).unwrap();
        assert!(ceilings.is_ok(), "{ceilings:?}");
        let orphans: wire::Answer<Vec<ResourceId>> = wire::decode(&dispatch(
            &Canned,
            host,
            ops::ORPHANS_FOR,
            &call(vec!["estate/acme".to_string()]),
        ))
        .unwrap();
        assert_eq!(
            orphans,
            Err(WireCloudError::Refused("no account".into())),
            "an account it cannot list is said, not an empty list"
        );
        let released: wire::Answer<Reclaimed> = wire::decode(&dispatch(
            &Canned,
            host,
            ops::RELEASE_FOR,
            &call(vec!["estate/acme".to_string()]),
        ))
        .unwrap();
        assert_eq!(released, Err(WireCloudError::Refused("no account".into())));
        let set = grow::DataSet {
            product: "gunnar".into(),
            server: ServerId("s".into()),
            members: vec![],
            target_gib: 50,
        };
        for (op, payload) in [
            (ops::PRICE_GROW, call(set.clone())),
            (ops::SURVEY_SET, call(set.clone())),
            (
                ops::RESUME_GROW,
                call(wire::GrowReq {
                    set,
                    records: vec![],
                }),
            ),
        ] {
            let a: wire::Answer<serde_json::Value> =
                wire::decode(&dispatch(&Canned, host, op, &payload)).unwrap();
            assert!(
                matches!(&a, Err(WireCloudError::Refused(m)) if m.contains("canned")),
                "{}: {a:?}",
                ops::name(op).unwrap()
            );
        }
        for (op, payload, verb) in [
            (
                ops::KEEP_BOOT,
                call(wire::KeepBootReq {
                    keep: monetize_cloud::lay::KeepBoot {
                        reference: "estate/acme".into(),
                        zone: Zone("se-sto1".into()),
                        machine: laid_machine(),
                        medium: String::new(),
                        medium_object: None,
                    },
                    records: vec![],
                }),
                "keep_boot",
            ),
            (
                ops::SET_ESTATE_FIREWALL,
                call(laid()),
                "set_estate_firewall",
            ),
            (ops::LABEL_ADOPTED, call(laid()), "label_adopted"),
        ] {
            let a: wire::Answer<serde_json::Value> =
                wire::decode(&dispatch(&Canned, host, op, &payload)).unwrap();
            assert!(
                matches!(&a, Err(WireCloudError::Refused(m)) if m.contains(verb)),
                "{verb}: {a:?}"
            );
        }
        let forgot: wire::Answer<Vec<String>> = wire::decode(&dispatch(
            &Canned,
            host,
            ops::FORGET_MEDIA,
            &call(wire::ForgetReq {
                laid: laid(),
                records: vec![],
            }),
        ))
        .unwrap();
        assert_eq!(forgot, Ok(vec![]), "nothing to forget is an answer");
    }

    /// The pool ledger names where it lives, and the pace is the host's clock and wait.
    #[test]
    fn the_ledger_says_where_it_lives_and_the_pace_is_the_hosts_clock() {
        struct Clock;
        impl Host for Clock {
            fn clock_unix_ms(&self) -> u64 {
                1_800_000_000_000
            }
        }
        let host: HostRef = &Clock;
        use monetize_cloud::pace::Pace as _;
        use monetize_cloud::pool::LedgerStore as _;
        assert_eq!(
            HostLedger {
                host,
                key: "pool".into()
            }
            .describe(),
            "host state `pool`"
        );
        let pace = HostPace(host);
        assert_eq!(pace.now_ms(), 1_800_000_000_000);
        pace.sleep(Duration::from_millis(1));
        // a host without the state door: the ledger's load is Unavailable, by its key
        let e = HostLedger {
            host: &NoHost,
            key: "pool".into(),
        }
        .load()
        .unwrap_err();
        assert!(
            matches!(&e, CloudError::Unavailable(m) if m.contains("host state `pool`")),
            "{e:?}"
        );
    }
}