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

nordisk/monetize

crates/monetize-product-wasm/src/load.rs

//! **Loading a guest**: vouching, limits, the module, the ABI, the manifest,
//! the gates. Everything before the first real verb; every failure refused by
//! name, wearing `[products.<id>]`.

use std::path::PathBuf;
use std::time::Duration;

use guest_host::{exports, ModuleTrust, PluginLimits};
use guest_host::module::LoadedModule;
use monetize_product::{GuestSpec, Meter, ProductBuild};
use monetize_product_guest::{abi, grants, imports, kinds, ops, wire, Answer, Manifest, WireMeter};

/// **Which block a guest is loaded for**: its section (`products`, `vendors`),
/// its id, its guest keys, and the kind of module it must be. Every refusal
/// wears `[<section>.<id>]`.
#[derive(Clone, Copy)]
pub struct Block<'a> {
    pub section: &'a str,
    pub id: &'a str,
    pub spec: Option<&'a GuestSpec>,
    /// [`kinds`]: what the module's manifest must declare.
    pub kind: &'a str,
}

impl Block<'_> {
    pub fn err(&self, what: impl std::fmt::Display) -> String {
        format!("[{}.{}] {what}", self.section, self.id)
    }
    /// The block a `[products.<id>]` build describes.
    pub fn product<'a>(b: &'a ProductBuild<'a>) -> Block<'a> {
        Block { section: "products", id: b.id, spec: b.guest, kind: kinds::PRODUCT }
    }
}

/// The product kind's fuel ceiling: 2e9 instructions per call. A poll parses
/// one product's listing for one tenant; a JSON round trip over ten thousand
/// rows is a few hundred million. An operator narrows it with `guest_fuel`,
/// never widens it.
pub const PRODUCT_FUEL_CEILING: u64 = 2_000_000_000;

/// The product kind's wall-clock ceiling: the doors' own 30 s timeout, twice,
/// so a guest making two calls to a slow box is cut off by name rather than
/// left hanging the poller.
pub const PRODUCT_CALL_TIMEOUT: Duration = Duration::from_secs(60);

/// A guest through step 6 of the load order: verified, compiled, negotiated,
/// its manifest read and gated. Not yet a `Product` — that needs the doors.
pub struct LoadedGuest {
    pub module: LoadedModule,
    pub spec: GuestSpec,
    pub manifest: Manifest,
    pub abi: u32,
    /// Every `env` import the module really has.
    pub real_imports: Vec<String>,
}

/// Pure: **a guest's limits** — the kind's fuel ceiling IS its fuel unless the block narrows it
/// (`guest_fuel`); the everyday call timeout and memory are the host's defaults unless the
/// block says; nothing passes the kind's ceilings. (test2 2026-10-12: the cloud kind ran under
/// the host's generic 1e9 instead of its own 2e10, and a cold UpCloud price list did not parse
/// within it — every bundle and every ladder step was refused "burned its whole CPU fuel".)
pub fn kind_limits(fuel_ceiling: u64, call_timeout: Duration, fuel: Option<u64>, call_timeout_ms: Option<u64>, max_memory_bytes: Option<u64>) -> PluginLimits {
    let mut limits = PluginLimits::default().narrowed(fuel_ceiling, call_timeout);
    limits.fuel = fuel.unwrap_or(fuel_ceiling);
    if let Some(ms) = call_timeout_ms {
        limits.call_timeout = Duration::from_millis(ms);
    }
    if let Some(m) = max_memory_bytes {
        limits.max_memory_bytes = m as usize;
    }
    limits.narrowed(fuel_ceiling, call_timeout)
}

impl LoadedGuest {
    /// Steps 1–6 of the load order in the crate docs.
    ///
    /// A block naming its `plugin` (G15: `[products.gunnar-test2] plugin =
    /// "gunnar"`) loads a module whose manifest names the PLUGIN.
    pub fn load(b: &ProductBuild<'_>) -> Result<LoadedGuest, String> {
        match b.guest.and_then(|g| g.plugin.as_deref()) {
            Some(plugin) => LoadedGuest::load_block_named(Block::product(b), plugin, PRODUCT_FUEL_CEILING, PRODUCT_CALL_TIMEOUT),
            None => LoadedGuest::load_block(Block::product(b)),
        }
    }

    /// [`LoadedGuest::load`] for any kind of block, under the product kind's
    /// ceilings (a vendor's calls are a product poll's size).
    pub fn load_block(b: Block<'_>) -> Result<LoadedGuest, String> {
        LoadedGuest::load_block_within(b, PRODUCT_FUEL_CEILING, PRODUCT_CALL_TIMEOUT)
    }

    /// [`LoadedGuest::load_block`] under a kind's OWN ceilings, which the
    /// block may only narrow — a cloud waits minutes for a server to start,
    /// and a product poll must not be allowed to.
    pub fn load_block_within(b: Block<'_>, fuel_ceiling: u64, call_timeout: Duration) -> Result<LoadedGuest, String> {
        let manifest_name = b.id;
        LoadedGuest::load_block_named(b, manifest_name, fuel_ceiling, call_timeout)
    }

    /// [`LoadedGuest::load_block_within`] for a block whose id is not the
    /// name the module's manifest carries: a vendor INSTANCE
    /// (`[vendors.stripe.gunnar]`, id `stripe.gunnar`) loads a module whose
    /// manifest names its KIND (`stripe`).
    pub fn load_block_named(b: Block<'_>, manifest_name: &str, fuel_ceiling: u64, call_timeout: Duration) -> Result<LoadedGuest, String> {
        LoadedGuest::load_block_accepting(b, manifest_name, &|name| name == manifest_name, fuel_ceiling, call_timeout)
    }

    /// [`LoadedGuest::load_block_named`] for a kind whose block may load more than one
    /// manifest name: `accept` says which (a DNS writer's `[dns_writers.cloudflare-holger]`
    /// loads the `cloudflare` guest, lane F1). `manifest_name` is what the refusal names.
    pub fn load_block_accepting(b: Block<'_>, manifest_name: &str, accept: &dyn Fn(&str) -> bool, fuel_ceiling: u64, call_timeout: Duration) -> Result<LoadedGuest, String> {
        let spec = b.spec.ok_or_else(|| {
            b.err("is built by the guest host but names no `guest = \"<path>.wasm\"`; a linked plugin has to be registered under this id instead")
        })?;
        let path: PathBuf = spec.module.clone();
        let err = |what: String| b.err(format!("guest {}: {what}", path.display()));

        // 1. Somebody must vouch — with a SIGNATURE (owner 2026-10-17: "monetize skall
        // verifiera signaturen på wasmen"). A digest pins bytes; it does not say who built them.
        if spec.trust_roots.is_empty() {
            return Err(err(
                "[guest-unsigned] nobody vouches for these bytes with a signature: the block names no \
                 `guest_trust_roots` (DER roots its .p7s must chain to). A `guest_digest` alone pins bytes \
                 without saying who built them; every guest monetize runs is signed, and a file in a \
                 directory is not a reason to run it inside the server"
                    .into(),
            ));
        }
        let bytes = std::fs::read(&path).map_err(|e| err(format!("could not be read: {e}")))?;

        // 2. The digest.
        let have = guest_host::digest_hex(&bytes);
        if let Some(want) = &spec.digest {
            if !want.trim().eq_ignore_ascii_case(&have) {
                return Err(err(format!(
                    "the module on disk hashes to blake3 {have}, and the block pins guest_digest {}. These are \
                     not the bytes the operator vouched for; refused. If the guest was rebuilt on purpose, \
                     pin the new digest",
                    want.trim()
                )));
            }
        }

        // 3. The trust policy (verified inside from_bytes, BEFORE compilation): always required.
        let trust = ModuleTrust::required(spec.trust_roots.iter().cloned());

        // 4. The limits: the kind's ceilings, narrowed by the block.
        let limits = kind_limits(fuel_ceiling, call_timeout, spec.fuel, spec.call_timeout_ms, spec.max_memory_bytes);
        limits.validate().map_err(|e| err(e.to_string()))?;

        // 5. Compile, exports, imports.
        let module = LoadedModule::from_bytes(path.clone(), &bytes, &limits, &trust, exports::ALL).map_err(|e| err(e.to_string()))?;
        module.refuse_foreign_imports(imports::MODULE).map_err(|e| err(e.to_string()))?;
        let real_imports = module.imports_from(imports::MODULE);
        for imp in &real_imports {
            match grants::for_import(imp) {
                None => {
                    if let Some(g) = grants::retired_import(imp) {
                        return Err(err(format!(
                            "[guest-door-retired] imports `env::{imp}`, the retired `{g}` door ({}); rebuild the guest against the current SDK",
                            grants::retired_why(g)
                        )));
                    }
                    return Err(err(format!(
                        "imports `env::{imp}`, which is not a monetize host door at all; the doors are {}",
                        imports::ALL.join(", ")
                    )))
                }
                Some("") => {}
                Some(g) if spec.grants.iter().any(|x| x == g) => {}
                Some(g) => {
                    return Err(err(format!(
                        "imports `{imp}`, which needs the grant `{g}`. This block grants [{}]. The import is \
                         therefore NOT DEFINED on the linker and the module could not instantiate; add \
                         `{g}` to `grants` or use a guest that does not ask for it",
                        spec.grants.join(", ")
                    )))
                }
            }
        }
        retired_doors(&spec.grants).map_err(err)?;
        for g in &spec.grants {
            if grants::imports(g).is_empty() {
                return Err(err(format!("`grants` names `{g}`, which is not a host door; the doors are {}", grants::VOCABULARY.join(", "))));
            }
        }
        check_secrets(spec).map_err(err)?;

        // 6. ABI and manifest, through a store with NO doors: reading a
        // manifest must not be able to reach the network.
        let probe = crate::product::probe_linker(&module);
        let abi_range = {
            let mut inst = module.instantiate(crate::product::probe_state(&module).map_err(err)?, &probe).map_err(|e| err(e.to_string()))?;
            module.call_u64(&mut inst, exports::PLUGIN_ABI).map_err(|e| err(e.to_string()))?
        };
        let abi = abi::negotiate((abi::ABI_MIN, abi::ABI_MAX), abi::unpack(abi_range)).map_err(err)?;
        let manifest: Manifest = {
            let mut inst = module.instantiate(crate::product::probe_state(&module).map_err(err)?, &probe).map_err(|e| err(e.to_string()))?;
            let raw = module.call_ptr(&mut inst, exports::MANIFEST).map_err(|e| err(e.to_string()))?;
            wire::decode(&raw).map_err(|e| err(format!("plugin_manifest answered undecodable bytes: {e}")))?
        };
        if !kinds::loads(b.kind, &manifest.kind) {
            return Err(err(format!(
                "[guest-wrong-kind] its manifest says it is a `{}` guest, and [{}.{}] loads `{}` guests; refused",
                manifest.kind, b.section, b.id, b.kind
            )));
        }
        if !accept(&manifest.product) {
            return Err(err(format!(
                "its manifest says it is `{}`, and this block [{}.{}] loads `{}`. A guest under \
                 the wrong id would meter one product's tenants as another's, or take one vendor's money as another's; refused",
                manifest.product, b.section, b.id, manifest_name
            )));
        }
        // The kind gate, on the kind the module IS (a products block loads a test guest too).
        kind_doors(&manifest.kind, &spec.grants, &manifest.grants, manifest.identity).map_err(err)?;
        // Gate A: everything the manifest declares must be granted.
        for g in &manifest.grants {
            if !spec.grants.iter().any(|x| x == g) {
                return Err(err(format!(
                    "its manifest declares the grant `{g}`, and this block grants [{}]. Add it, or use a \
                     guest that does not declare it",
                    spec.grants.join(", ")
                )));
            }
        }
        // Gate B: every real import must be one the manifest declared. A
        // manifest that understates the module is refused.
        for imp in &real_imports {
            if let Some(g) = grants::for_import(imp).filter(|g| !g.is_empty()) {
                if !manifest.grants.iter().any(|x| x == g) {
                    return Err(err(format!(
                        "imports `{imp}` but its manifest does not declare the grant `{g}` — the module asks \
                         for more than it admits to; refused"
                    )));
                }
            }
        }
        Ok(LoadedGuest { module, spec: spec.clone(), manifest, abi, real_imports })
    }
}

/// **Which guest these bytes are**: the `(kind, name)` its manifest declares (`("vendor",
/// "stripe")`), read through a store with NO doors, under the product kind's ceilings. For a
/// caller that has ALREADY verified the bytes (the plugin update: the release list's digest
/// and the module's own signature) and must know which blocks they belong to; it vouches for
/// nothing itself.
pub fn identify(bytes: &[u8]) -> Result<(String, String), String> {
    let limits = kind_limits(PRODUCT_FUEL_CEILING, PRODUCT_CALL_TIMEOUT, None, None, None);
    let module = LoadedModule::from_bytes(PathBuf::from("identify.wasm"), bytes, &limits, &ModuleTrust::NotRequired, exports::ALL).map_err(|e| e.to_string())?;
    let probe = crate::product::probe_linker(&module);
    let mut inst = module.instantiate(crate::product::probe_state(&module)?, &probe).map_err(|e| e.to_string())?;
    let raw = module.call_ptr(&mut inst, exports::MANIFEST).map_err(|e| e.to_string())?;
    let manifest: Manifest = wire::decode(&raw).map_err(|e| format!("plugin_manifest answered undecodable bytes: {e}"))?;
    Ok((manifest.kind, manifest.product))
}

/// **The block's secrets, checked before a byte of the module runs**: every
/// `http_auth` host is on the allow-list (a credential for a host the door will
/// not reach is a mistake, and a credential for one it WILL reach unasked is
/// worse), every name is a name, and a block that names a secret has somewhere
/// to read it from. The files themselves are read at each use, not here, so a
/// secret written after the block is not a refusal.
pub fn check_secrets(spec: &GuestSpec) -> Result<(), String> {
    let mut named = 0usize;
    for a in &spec.http_auth {
        if !spec.http_hosts.iter().any(|h| h.trim_end_matches('/') == a.host.trim_end_matches('/')) {
            return Err(format!(
                "[http-auth-host-not-allowed] http_auth names {:?}, which is not in http_hosts [{}]; add it there or remove the entry",
                a.host,
                spec.http_hosts.join(", ")
            ));
        }
        if a.basic_user.is_some() && a.basic_user_secret.is_some() {
            return Err(format!("[http-auth] {:?}: basic_user and basic_user_secret are two answers to one question; name one", a.host));
        }
        if a.secret.is_none() && a.client_cert_secret.is_none() && a.basic_user.is_none() && a.basic_user_secret.is_none() {
            return Err(format!("[http-auth] {:?}: the entry adds nothing (no secret, no basic user, no client_cert_secret)", a.host));
        }
        for n in a.secret_names() {
            crate::secrets::valid_name(n)?;
            named += 1;
        }
    }
    for n in &spec.hmac_secrets {
        crate::secrets::valid_name(n)?;
        named += 1;
    }
    if !spec.hmac_secrets.is_empty() && !spec.grants.iter().any(|g| g == grants::HMAC) {
        return Err("[hmac-not-granted] hmac_secrets is set and `grants` does not name `hmac`; the guest could never use them".into());
    }
    if named > 0 && spec.secrets_dir.is_none() {
        return Err("[secret-dir] the block names secrets and the server has no secrets_dir to read them from".into());
    }
    Ok(())
}

/// A guest's meter table as the `&'static` slice the trait answers, interned:
/// names and meanings through [`crate::intern`], the table through one pool,
/// so reloading the same guest answers the slice already held.
pub fn leak_meters(meters: Vec<WireMeter>) -> &'static [Meter] {
    static TABLES: crate::Pool<Meter> = std::sync::Mutex::new(Vec::new());
    let v: Vec<Meter> = meters
        .into_iter()
        .map(|m| Meter { name: crate::intern(&m.name), meaning: crate::intern(&m.meaning), enforcement: m.enforcement, reports_usage: m.reports_usage, backing: m.backing, scope: m.scope })
        .collect();
    crate::intern_slice(&TABLES, &v)
}

/// **The kind gate** (T13 A28 M3; owner decision 6, 2026-10-26): a guest of `kind` is granted
/// only its own part of the vocabulary ([`grants::of_kind`]). A block that grants, or a manifest
/// that asks for, any other door is refused naming the kind and the grant; a retired door is
/// refused as retired; and no guest declares an identity (the host mints none).
pub fn kind_doors(kind: &str, block: &[String], manifest: &[String], identity: bool) -> Result<(), String> {
    if identity {
        return Err("[guest-identity-retired] its manifest declares an identity, and the host mints none since \
                    monetize v2.0 T13 (the `sign` door is gone for every kind of guest); rebuild the guest \
                    against the current SDK"
            .into());
    }
    retired_doors(block)?;
    retired_doors(manifest)?;
    let Some(own) = grants::of_kind(kind) else {
        return Err(format!("[guest-grant-outside-kind] its manifest says it is a `{kind}` guest, a kind the host grants no doors to; refused"));
    };
    for (whose, list) in [("the block grants", block), ("its manifest declares", manifest)] {
        if let Some(g) = list.iter().find(|g| !own.contains(&g.as_str())) {
            let why = if kind == kinds::PRODUCT {
                " The six-op product contract (monetize v2.0 T13) dials nothing: the ONE box pulls every product \
                 from its estate and checks actor tickets on the host. Write `grants = []`"
            } else {
                ""
            };
            return Err(format!(
                "[guest-grant-outside-kind] {whose} `{g}`, and a `{kind}` guest may be granted only [{}].{why}",
                own.join(", ")
            ));
        }
    }
    Ok(())
}

/// **A retired door is refused as retired**, before anything else reads the list.
pub fn retired_doors(list: &[String]) -> Result<(), String> {
    match list.iter().find(|g| grants::RETIRED.contains(&g.as_str())) {
        Some(g) => Err(format!("[guest-door-retired] `{g}` is no longer a host door ({}); remove it from `grants`", grants::retired_why(g))),
        None => Ok(()),
    }
}

/// The answer of a `plugin_call`, decoded.
pub fn decode_answer<T: serde::de::DeserializeOwned>(op: u32, raw: &[u8]) -> Result<Answer<T>, String> {
    wire::decode(raw).map_err(|e| format!("op {} ({}): the guest answered undecodable bytes: {e}", op, ops::name(op).unwrap_or("?")))
}

#[cfg(test)]
mod kind_doors_tests {
    use super::kind_doors;
    use monetize_product_guest::kinds;

    fn v(g: &[&str]) -> Vec<String> {
        g.iter().map(|s| s.to_string()).collect()
    }

    /// ★ T13 (A28 M3): **a product guest opens no gRPC or signing door**, whether the block grants
    /// it or the manifest asks for it; refused by name.
    #[test]
    fn a_product_guest_granted_grpc_or_sign_is_refused_by_name() {
        for (block, manifest) in [(v(&["grpc"]), v(&[])), (v(&["grpc"]), v(&["grpc"])), (v(&["sign"]), v(&["sign"]))] {
            let e = kind_doors(kinds::PRODUCT, &block, &manifest, false).expect_err("refused");
            assert!((e.contains("[guest-grant-outside-kind]") || e.contains("[guest-door-retired]")) && (e.contains("`grpc`") || e.contains("`sign`")), "{e}");
        }
        assert!(kind_doors(kinds::PRODUCT, &v(&[]), &v(&[]), false).is_ok(), "the six-op product: no grants");
    }

    /// ★ Owner decision 6 (2026-10-26): **each kind is granted only its own set**; anything
    /// else is refused naming the kind and the grant, from the block or from the manifest.
    #[test]
    fn a_grant_outside_the_kinds_set_is_refused_naming_the_kind_and_the_grant() {
        for (kind, g) in [(kinds::PRODUCT, "state"), (kinds::PRODUCT, "http"), (kinds::VENDOR, "grpc"), (kinds::CLOUD, "hmac"), (kinds::DNS, "state"), (kinds::SIGNUP, "http"), (kinds::TEST, "grpc")] {
            for (block, manifest) in [(v(&[g]), v(&[])), (v(&[]), v(&[g]))] {
                let e = kind_doors(kind, &block, &manifest, false).expect_err("refused");
                assert!(e.contains("[guest-grant-outside-kind]") && e.contains(&format!("`{kind}`")) && e.contains(&format!("`{g}`")), "{kind}/{g}: {e}");
            }
        }
        for kind in monetize_product_guest::kinds::ALL {
            let own = v(monetize_product_guest::grants::of_kind(kind).unwrap());
            assert!(kind_doors(kind, &own, &own, false).is_ok(), "{kind} keeps its own set");
        }
        assert!(kind_doors("nonsense", &v(&[]), &v(&[]), false).unwrap_err().contains("`nonsense`"), "a kind with no set is refused by name");
    }

    /// ★ T13 (A28 M3): **no guest declares an identity**: the host mints none, for a product or a
    /// signup; refused by name.
    #[test]
    fn a_guest_that_declares_an_identity_is_refused_by_name() {
        for kind in [kinds::PRODUCT, kinds::SIGNUP] {
            let e = kind_doors(kind, &v(&[]), &v(&[]), true).expect_err("refused");
            assert!(e.contains("[guest-identity-retired]"), "{kind}: {e}");
        }
    }

    /// The signup-holger shape (grpc + state, no identity) still loads: the signup host keeps
    /// its gRPC door (Holger's base plan). The real module through the real host is
    /// monetize-impl `guests/signup-holger/tests/the_guest_grants_the_base_plan.rs`.
    #[test]
    fn a_signup_guest_keeps_its_grpc_door() {
        assert!(kind_doors(kinds::SIGNUP, &v(&["grpc", "state"]), &v(&["grpc", "state"]), false).is_ok());
    }
}

#[cfg(test)]
mod intern_tests {
    use super::*;
    use monetize_product::Enforcement;

    fn table() -> Vec<WireMeter> {
        vec![WireMeter::new("intern-test-seats", "seats in use", Enforcement::Enforced), WireMeter::new("intern-test-gb", "storage", Enforcement::Measured)]
    }

    /// ★ test2 2026-10-12: **a guest runs under its KIND's fuel ceiling unless its block narrows
    /// it** — the cloud kind's 2e10, not the host's generic 1e9 (a cold UpCloud price list did
    /// not parse within 1e9: every bundle and ladder step refused). A block's `guest_fuel`
    /// narrows it; nothing passes the ceiling.
    #[test]
    fn a_guest_runs_under_its_kinds_fuel_ceiling_unless_the_block_narrows_it() {
        let t = Duration::from_secs(900);
        assert_eq!(super::kind_limits(20_000_000_000, t, None, None, None).fuel, 20_000_000_000, "the cloud kind's own ceiling");
        assert_eq!(super::kind_limits(super::PRODUCT_FUEL_CEILING, t, None, None, None).fuel, super::PRODUCT_FUEL_CEILING);
        assert_eq!(super::kind_limits(20_000_000_000, t, Some(3_000_000_000), None, None).fuel, 3_000_000_000, "the block narrows");
        assert_eq!(super::kind_limits(20_000_000_000, t, Some(u64::MAX), None, None).fuel, 20_000_000_000, "never past the ceiling");
    }

    #[test]
    fn a_reload_with_the_same_meters_leaks_nothing_new() {
        let first = leak_meters(table());
        for _ in 0..100 {
            let again = leak_meters(table());
            assert!(std::ptr::eq(first, again), "a reload of the same meter table must answer the slice already held");
            assert!(std::ptr::eq(first[0].name, again[0].name));
        }
        assert!(std::ptr::eq(crate::intern("intern-test-seats"), first[0].name), "meter names share the id interner");
    }

    #[test]
    fn the_same_id_interned_twice_is_one_allocation() {
        let a = crate::intern(&String::from("intern-test-id"));
        let b = crate::intern(&String::from("intern-test-id"));
        assert!(std::ptr::eq(a, b));
    }
}