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

nordisk/monetize

guests/toy/src/lib.rs

//! **The toy guest**: the six ops of the product contract, every answer checkable. It is a TEST
//! guest (`kinds::TEST`, owner decision 6, 2026-10-26): a products block loads it, and its grants
//! are the test kind's own, never a product's. Its one door is the typed `endpoint` (the block's
//! address, and nothing else: the door takes no name): its refusing mode reads it, and company
//! `where` answers it back. A product guest that ships has no grants; the toy holds the typed door
//! only to prove it. (It held the generic `env` door until that was retired.)

use std::collections::BTreeMap;

use monetize_product_guest::{
    kinds, Backing, Enforcement, Guest, Host, Manifest, ProductError, WireMeter,
};

#[derive(Default)]
pub struct ToyGuest;

pub const PRODUCT: &str = "toy";

/// **The refusing mode** (lane L16, 2026-10-18): a block whose `endpoint` is
/// `lifecycle-of:<kind>` makes the toy bring the lifecycle facts of `<kind>` and REFUSE to make
/// a lay plan — as Holger's own guest does (its estates are laid by monetize's native
/// `lifecycle::Holger`). What proves the core falls back to the native lifecycle of that kind.
pub const REFUSING: &str = "lifecycle-of:";

/// The kind the toy stands in for, when its block puts it in the refusing mode.
fn refusing_kind(host: &dyn Host) -> Option<String> {
    host.endpoint()
        .ok()
        .and_then(|e| e.strip_prefix(REFUSING).map(str::to_string))
        .filter(|k| !k.trim().is_empty())
}

impl Guest for ToyGuest {
    fn manifest(&self) -> Manifest {
        // A TEST guest (owner decision 6): a products block loads it, and its grants are the test
        // kind's own, never a product's.
        Manifest::of_kind(
            kinds::TEST,
            PRODUCT,
            &[monetize_product_guest::grants::ENDPOINT],
        )
        .with_meters(vec![
            WireMeter::new(
                "things",
                "how many things the tenant holds",
                Enforcement::Measured,
            ),
            WireMeter::new("bytes", "bytes the things take", Enforcement::Enforced)
                .with_backing(Backing::DiskBytes),
            WireMeter::cap_only("cpu_millicores", "bought iron", Enforcement::Provisioned)
                .with_backing(Backing::CpuMillicores)
                .per_product(),
        ])
    }

    /// An estate of ONE machine (`solo`), from its own media (`toy-media`): what proves a
    /// product guest brings its own estate lifecycle across the ABI (2026-10-17). Its main mints
    /// actor tickets (the host checks them), no test buttons, no guardian.
    fn lifecycle_facts(
        &self,
        host: &dyn Host,
    ) -> Result<monetize_product_guest::wire::LifecycleFacts, ProductError> {
        Ok(monetize_product_guest::wire::LifecycleFacts {
            kind: refusing_kind(host).unwrap_or_else(|| PRODUCT.into()),
            update_order: vec!["solo".into()],
            app_medium: "toy-media".into(),
            front_medium: "toy-media".into(),
            front_role: "solo".into(),
            keep_medium: "toy-keep".into(),
            front_keep_medium: "toy-keep".into(),
            upgraded_by_shop: Vec::new(),
            medium_of: BTreeMap::from([("solo".to_string(), "toy-media".to_string())]),
            other_medium: "toy-media".into(),
            backup_roles: vec!["solo".into()],
            words: monetize_product_guest::wire::WireWords {
                name: "toy box".into(),
                title: "Toy box".into(),
                account_home: "toy.example".into(),
            },
            default_shape: serde_json::Value::Null,
            data_roles: vec![monetize_product_guest::wire::WireDataRole {
                role: "solo".into(),
                mirrors_primary: false,
                set_suffix: "-data".into(),
            }],
            domain_proof_name: "toy-box-proof".into(),
            // One machine, updated from its keep medium: no live slot (B4 proves the wire carries it).
            live_slot: false,
            tickets: true,
            ..Default::default()
        })
    }

    /// The plan: `solo-{company}` in the config's zone, its system disk in the template's tier
    /// when one is handed in, the config's labels on it. Company `where` answers the typed
    /// `endpoint` door back (see the crate's notes).
    fn lifecycle_lay_plan(
        &self,
        host: &dyn Host,
        req: &monetize_product_guest::wire::LayPlanReq,
    ) -> Result<serde_json::Value, ProductError> {
        if let Some(kind) = refusing_kind(host) {
            return Err(ProductError::Refused(format!("the toy lays nothing for {}: a {kind} estate is laid by monetize's native lifecycle", req.company)));
        }
        let mut labels = req
            .config
            .get("labels")
            .cloned()
            .unwrap_or_else(|| serde_json::json!({}));
        labels["role"] = serde_json::json!("solo");
        // Where this deployment's box is: a guest is built once and signed once, so it cannot be
        // baked in. The typed door answers the block's address; there is no name to ask it for
        // anything else.
        if req.company == "where" {
            return Err(ProductError::Refused(format!(
                "endpoint={:?}",
                host.endpoint().map_err(ProductError::Refused)?
            )));
        }
        let tier = req
            .template_tiers
            .get("toy-media")
            .cloned()
            .unwrap_or_else(|| "standard".into());
        Ok(serde_json::json!({
            "reference": format!("{}-estate", req.company),
            "company": req.company,
            "zone": req.config.get("zone").cloned().unwrap_or_default(),
            "machines": [{
                "role": "solo", "hostname": format!("solo-{}", req.company), "title": "", "plan": "STARTER-1xCPU-1GB",
                "boot_order": "disk,cdrom", "labels": labels, "interfaces": [], "medium": "",
                "disks": [{ "title": "system", "gib": 10, "tier": tier, "labels": {} }]
            }]
        }))
    }
}

monetize_product_guest::export_product!(ToyGuest, grants = [endpoint]);