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));
}
}