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

nordisk/holger

plugin/abi/src/v2.rs

//! **ABI v2** — the same length-prefixed codec, widened so a guest can be
//! something other than a package handler.
//!
//! v1 ([`crate::wire`]) is package-handler-shaped: six per-verb exports, one
//! implicit and ungrantable store capability, and a manifest whose decoder calls
//! `expect_end`, so the concept of an unknown field does not exist. None of that
//! can carry a meter. v2 adds, and removes nothing:
//!
//! | v1 | v2 |
//! |---|---|
//! | `ABI_VERSION: u32`, compared with `!=` | [`AbiRange`] on both sides, [`negotiate`]d once, echoed in [`WireCallContext::abi`] |
//! | positional manifest, `expect_end` | a TLV manifest: an unknown **optional** tag is carried, an unknown **required** tag is refused by tag number |
//! | one implicit store capability | [`Capability`], declared in the manifest, granted per module from a config file, refused by name at load |
//! | six per-verb exports | one `plugin_call(op, ptr, len)`, an op space partitioned by [`GuestKind`] |
//! | `u64` counters | [`ReadingValue`] — an uncounted zero cannot be spelled |
//!
//! Everything v1 **bounded** is untouched: fuel 1e9, 256 MiB, a 30 s epoch
//! deadline, no WASI, and the `.wasm` verified by detached CMS over blake3
//! before a byte of it is compiled.
//!
//! # What is deliberately absent from [`WireAuditRecord`]
//!
//! No timestamp, no identity, no source address. The host stamps all three from
//! the call it is already inside, so a guest cannot write a record naming
//! somebody else or claiming another time.

use crate::codec::{DecodeError, Reader, Writer};

// ───────────────────────────── versioning ─────────────────────────────

/// The oldest ABI this host speaks. v1 modules keep loading.
pub const HOST_ABI_MIN: u32 = 1;
/// The newest ABI this host speaks.
pub const HOST_ABI_MAX: u32 = 2;

/// What a side speaks: a RANGE, not a point. v1's single `u32` compared with
/// `!=` has no forward direction at all; this has one in both directions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AbiRange {
    pub min: u32,
    pub max: u32,
}

impl AbiRange {
    pub const fn new(min: u32, max: u32) -> Self {
        Self { min, max }
    }

    /// This host's range.
    pub const fn host() -> Self {
        Self {
            min: HOST_ABI_MIN,
            max: HOST_ABI_MAX,
        }
    }

    /// `min << 32 | max` — what the `plugin_abi` export returns, so the host can
    /// read a guest's range without allocating or decoding anything.
    pub const fn pack(self) -> u64 {
        ((self.min as u64) << 32) | (self.max as u64)
    }

    pub const fn unpack(packed: u64) -> Self {
        Self {
            min: (packed >> 32) as u32,
            max: (packed & 0xffff_ffff) as u32,
        }
    }

    pub fn encode(&self, w: &mut Writer) {
        w.u32(self.min).u32(self.max);
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            min: r.u32("AbiRange.min")?,
            max: r.u32("AbiRange.max")?,
        })
    }
}

/// The one negotiation. Refuses with BOTH ranges in the message, because
/// "version mismatch" without the two numbers is a bug report nobody can act on.
pub fn negotiate(host: AbiRange, guest: AbiRange) -> Result<u32, String> {
    let lo = host.min.max(guest.min);
    let hi = host.max.min(guest.max);
    if lo > hi {
        return Err(format!(
            "no common ABI: this host speaks {}..={}, the module speaks {}..={}",
            host.min, host.max, guest.min, guest.max
        ));
    }
    Ok(hi)
}

// ───────────────────────────── capabilities ─────────────────────────────

/// What a module may reach. Declared by the module, granted by the operator,
/// refused **by name** at load when the two disagree.
///
/// Two of these are not imports at all: `PrincipalRead` and `DigestRead` decide
/// whether a FIELD of [`WireCallContext`] is populated. They are still
/// capabilities, because "who is calling" and "which exact bytes" are the two
/// things a guest can do the most harm with.
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub enum Capability {
    /// `host_store_get`, `host_store_size`
    StoreRead,
    /// `host_store_put`
    StoreWrite,
    /// `host_store_list`
    StoreList,
    /// `host_usage_read` — the counters
    UsageRead,
    /// `host_audit_write` — one record, host-stamped
    AuditWrite,
    /// `host_clock` — a trusted reading, carrying its SOURCE
    ClockRead,
    /// [`WireCallContext::principal`] is populated
    PrincipalRead,
    /// [`WireArtifactRef::digest`] is populated
    DigestRead,
}

impl Capability {
    /// Every capability, in wire order. The loader iterates this; nothing
    /// anywhere else writes a capability list by hand.
    pub const ALL: &'static [Capability] = &[
        Capability::StoreRead,
        Capability::StoreWrite,
        Capability::StoreList,
        Capability::UsageRead,
        Capability::AuditWrite,
        Capability::ClockRead,
        Capability::PrincipalRead,
        Capability::DigestRead,
    ];

    /// The v1 bundle. An ABI-1 module declares nothing, so the host grants it
    /// exactly what v1 gave every module unconditionally — the one backward
    /// compatibility rule, written down here so it is a decision and not a leak.
    pub const V1_IMPLICIT: &'static [Capability] = &[
        Capability::StoreRead,
        Capability::StoreWrite,
        Capability::StoreList,
    ];

    /// The stable wire/config spelling. ONE writer.
    pub fn name(self) -> &'static str {
        match self {
            Capability::StoreRead => "store.read",
            Capability::StoreWrite => "store.write",
            Capability::StoreList => "store.list",
            Capability::UsageRead => "usage.read",
            Capability::AuditWrite => "audit.write",
            Capability::ClockRead => "clock.read",
            Capability::PrincipalRead => "principal.read",
            Capability::DigestRead => "digest.read",
        }
    }

    /// The inverse. An unknown name is `None` — the caller decides whether that
    /// is a config error (it is) or something to skip (it is not).
    pub fn from_name(s: &str) -> Option<Capability> {
        Capability::ALL.iter().copied().find(|c| c.name() == s)
    }

    /// The `env::` import names this capability unlocks. **Empty** for the two
    /// capabilities that are fields rather than imports — and empty is not a
    /// weakness here: the host simply does not fill the field.
    pub fn imports(self) -> &'static [&'static str] {
        match self {
            Capability::StoreRead => &[imports::STORE_GET, imports::STORE_SIZE],
            Capability::StoreWrite => &[imports::STORE_PUT],
            Capability::StoreList => &[imports::STORE_LIST],
            Capability::UsageRead => &[imports::USAGE_READ],
            Capability::AuditWrite => &[imports::AUDIT_WRITE],
            Capability::ClockRead => &[imports::CLOCK],
            Capability::PrincipalRead | Capability::DigestRead => &[],
        }
    }

    /// Which capability an import name belongs to, or `None` if the name is
    /// outside the whole vocabulary — which is [`GrantError::UnknownImport`].
    pub fn for_import(import: &str) -> Option<Capability> {
        Capability::ALL
            .iter()
            .copied()
            .find(|c| c.imports().contains(&import))
    }

    pub fn encode(self, w: &mut Writer) {
        w.str(self.name());
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let s = r.str("Capability")?;
        Capability::from_name(&s).ok_or(DecodeError::NotUtf8 {
            field: "Capability",
        })
    }
}

/// The `env` imports of ABI v2. The four v1 names are unchanged — a v1 module's
/// import list is still a subset of the vocabulary.
pub mod imports {
    pub use crate::imports::{MODULE, STORE_GET, STORE_LIST, STORE_PUT, STORE_SIZE};

    /// `host_usage_read(repo_ptr, repo_len) -> u64` → encoded
    /// `Result<Vec<WireMeterReading>, String>`.
    pub const USAGE_READ: &str = "host_usage_read";
    /// `host_audit_write(ptr, len) -> u64` → encoded `Result<(), String>`.
    ///
    /// It returns the `Result` rather than swallowing it, because a guest whose
    /// verdict depended on the record landing must be able to refuse.
    pub const AUDIT_WRITE: &str = "host_audit_write";
    /// `host_clock() -> u64` → encoded `Result<WireInstant, String>`.
    pub const CLOCK: &str = "host_clock";

    /// The whole v2 vocabulary. An import outside this is refused by name at
    /// load; an import inside it but not granted is **never defined on the
    /// linker**, so the module fails instantiation instead of getting a stub.
    pub const ALL_V2: &[&str] = &[
        STORE_GET,
        STORE_PUT,
        STORE_LIST,
        STORE_SIZE,
        USAGE_READ,
        AUDIT_WRITE,
        CLOCK,
    ];
}

/// The v2 export set: **fixed and kind-independent**, so a third guest kind is
/// one `KindSpec` const and not a second loader.
pub mod exports {
    /// `alloc(u32) -> u32` — v1, unchanged.
    pub const ALLOC: &str = "alloc";
    /// `dealloc(u32, u32)` — NEW. v1 leaks by design (a fresh `Store` per call
    /// makes that sound); a long-lived meter guest cannot.
    pub const DEALLOC: &str = "dealloc";
    /// `result_len() -> u32` — v1, unchanged.
    pub const RESULT_LEN: &str = "result_len";
    /// `plugin_abi() -> u64` — NEW. `min << 32 | max`, read before anything else.
    pub const PLUGIN_ABI: &str = "plugin_abi";
    /// `plugin_manifest() -> u32` — v1's name, now returning the TLV manifest.
    pub const MANIFEST: &str = "plugin_manifest";
    /// `plugin_call(op: u32, ptr: u32, len: u32) -> u64` — NEW, `ptr << 32 | len`.
    pub const PLUGIN_CALL: &str = "plugin_call";

    /// Every v2 export, in load-check order.
    pub const ALL_V2: &[&str] = &[
        ALLOC,
        DEALLOC,
        RESULT_LEN,
        PLUGIN_ABI,
        MANIFEST,
        PLUGIN_CALL,
    ];
}

// ───────────────────────────── kinds and ops ─────────────────────────────

/// The op space, partitioned so a kind's ops never collide.
///
/// ```text
///     0 ..=  99   common to every kind (0 = manifest, reserved)
///   100 ..= 199   PackageHandler
///   200 ..= 299   Meter
///   300 ..= 399   the third kind, whatever it is
/// ```
pub mod ops {
    // common
    pub const MANIFEST: u32 = 0;
    // PackageHandler
    pub const FETCH: u32 = 100;
    pub const PUT: u32 = 101;
    pub const LIST: u32 = 102;
    pub const COORDINATE_FOR_PATH: u32 = 103;
    pub const HTTP: u32 = 104;
    // Meter
    pub const METERS: u32 = 200;
    pub const READ: u32 = 201;
    pub const CAPACITY: u32 = 202;
    pub const SELFTEST: u32 = 203;
    pub const EVALUATE: u32 = 210;
    pub const ACCEPT_FACT: u32 = 211;
    pub const DESCRIBE_FACT: u32 = 212;
}

/// One op a kind answers. **Data**, so the loader checks a module against a
/// table instead of running a `match` that a third kind would have to be added to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpSpec {
    pub op: u32,
    pub name: &'static str,
    /// Ops a guest may decline with `Err`, as opposed to ops whose absence makes
    /// the module useless for its kind.
    pub optional: bool,
}

#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub enum GuestKind {
    /// v1's contract, renumbered into the op space.
    PackageHandler = 1,
    /// Readings, capacity, verdicts, facts.
    Meter = 2,
}

/// The contract between the host and a module of one kind: a name, the
/// capabilities no module of the kind can function without, and its op table.
///
/// The loader **looks a kind up in this table**; it never matches on the kind.
/// Adding a third kind is one const and one enum variant — the capability gates,
/// the limits, the verification and the runtimes are untouched.
#[derive(Debug)]
pub struct KindSpec {
    pub kind: GuestKind,
    pub name: &'static str,
    pub mandatory: &'static [Capability],
    pub ops: &'static [OpSpec],
    /// **CPU fuel one call into a guest of this kind may burn** — the kind's
    /// own ceiling, not a global one (?17, answered 2026-09-20).
    ///
    /// It is a CEILING and not a setting: the host takes
    /// `min(operator_configured_fuel, this)`, so a deployment may tighten it and
    /// may never widen it. A kind whose work is "one artifact's bytes" and a
    /// kind that answers a verdict on the push path do not want the same number,
    /// and before this field both got the package handler's.
    pub fuel: u64,
    /// **Wall-clock ceiling on one call into a guest of this kind**, in
    /// milliseconds, applied the same way: the effective deadline is the
    /// tighter of this and what the operator configured.
    ///
    /// Fuel counts instructions and does not tick while a guest is parked in a
    /// host import waiting on a slow disk; only the clock catches that. So this
    /// is the BACKSTOP, and it is deliberately the loose one — see
    /// [`METER_SPEC`] for why a meter's deadline was NOT tightened to 250 ms.
    pub call_timeout_ms: u64,
}

static PACKAGE_HANDLER_SPEC: KindSpec = KindSpec {
    kind: GuestKind::PackageHandler,
    name: "package-handler",
    // A handler that cannot read or write bytes is not a handler.
    mandatory: &[Capability::StoreRead, Capability::StoreWrite],
    ops: &[
        OpSpec {
            op: ops::FETCH,
            name: "fetch",
            optional: false,
        },
        OpSpec {
            op: ops::PUT,
            name: "put",
            optional: false,
        },
        OpSpec {
            op: ops::LIST,
            name: "list",
            optional: false,
        },
        OpSpec {
            op: ops::COORDINATE_FOR_PATH,
            name: "coordinate_for_path",
            optional: false,
        },
        OpSpec {
            op: ops::HTTP,
            name: "http",
            optional: false,
        },
    ],
    // Unchanged from what every guest got before kinds had their own ceiling:
    // 1e9 instructions, 30 s. A handler's job is one artifact's bytes in and out
    // plus a little parsing, and the two handlers in this tree spend six figures
    // of instructions on a real fetch — so this is four orders of magnitude of
    // headroom and still under a second of pure compute for a `loop {}`. Moving
    // it is a separate measurement and a separate ruling; ?17 was about meters.
    fuel: 1_000_000_000,
    call_timeout_ms: 30_000,
};

/// **What the open Meter guest actually costs, measured.**
///
/// MEASURED on oden 2026-09-20 by `plugin/host/tests/the_meter_fuel_ceiling.rs`
/// against the REAL `holger_plugin_allocation_wasm.wasm` that
/// `cargo xtask build-wasm-plugins` produces — release, wasmtime 48 — asking
/// wasmtime itself how much fuel each call consumed. Fuel for ONE call, which
/// includes the `alloc` of the argument and the `result_len` read in the same
/// store:
///
/// ```text
/// op                empty store    64 blobs / 16 rows   1024 blobs / 128 rows
/// plugin_abi              6 586                 6 586                   6 586
/// plugin_manifest         6 586                 6 586                   6 586
/// meters                 14 110                14 110                  14 110
/// read                    6 151                23 852                 164 574
/// capacity                6 174                 6 174                   6 174
/// selftest               11 518                58 398                 771 687
/// evaluate                2 636                 2 636                   2 636
/// accept_fact             2 559                 2 559                   2 559
/// describe_fact           2 703                 2 703                   2 703
/// ```
///
/// Three things the table says, and the ceiling is built on all three:
///
/// * **`evaluate` — the op on the PUT path — is the CHEAPEST of the nine**, at
///   2 636 fuel, flat under every load. The open guest's verdict is `Allow`
///   unconditionally and decodes nothing; the expensive ops are the reading
///   ones, and those are not on the push path.
/// * The two that scale are `read` and `selftest`, and what they scale with is
///   the SIZE OF THE ANSWER they decode — the host's rows and the store
///   listing. `selftest` is the worst at ~753 fuel per store key, because it
///   sums the whole listing to cross-check the host's figure.
/// * Everything else is flat, because it decodes a fixed-size answer.
pub const MEASURED_OPEN_GUEST_PEAK_FUEL: u64 = 771_687;

/// The measured peak, rounded UP to a whole million, which is what the ceiling
/// is arithmetic on.
///
/// Rounded rather than exact on purpose: a compiler bump moves these numbers by
/// single-digit percent, and a ceiling pinned to the last digit of one
/// measurement would be a gate that goes red on a toolchain upgrade rather than
/// on a defect. The rounding is the slack for that, and the test asserts the
/// real peak still fits inside it with the full multiple to spare.
const MEASURED_FUEL_PEAK: u64 = 1_000_000;

static METER_SPEC: KindSpec = KindSpec {
    kind: GuestKind::Meter,
    name: "meter",
    // A meter that cannot read the counters has nothing to say. Everything else
    // — the principal, the digest, the clock, the audit log — is per-deployment.
    mandatory: &[Capability::UsageRead],
    ops: &[
        OpSpec {
            op: ops::METERS,
            name: "meters",
            optional: false,
        },
        OpSpec {
            op: ops::READ,
            name: "read",
            optional: false,
        },
        OpSpec {
            op: ops::CAPACITY,
            name: "capacity",
            optional: false,
        },
        OpSpec {
            op: ops::SELFTEST,
            name: "selftest",
            optional: false,
        },
        OpSpec {
            op: ops::EVALUATE,
            name: "evaluate",
            optional: false,
        },
        OpSpec {
            op: ops::ACCEPT_FACT,
            name: "accept_fact",
            optional: true,
        },
        OpSpec {
            op: ops::DESCRIBE_FACT,
            name: "describe_fact",
            optional: true,
        },
    ],
    // ── ?17 ANSWERED 2026-09-20: MEASURE, then cap WIDE ──────────────────────
    //
    // The proposal on record was 10e6 fuel / 250 ms, one or two orders tighter
    // than the handler's, with a slow verdict REFUSING. The refusing part stands
    // — `Err → undecided → refuse` is gunnar's rule and `meter_gate.rs` already
    // implements it. The NUMBER does not, and the reason the number matters more
    // than it did when the proposal was written is that the meter gate now runs
    // on PUT (`Holger::meter_gates`). A ceiling that fires is therefore not a
    // safety control on a background reading, it is an AVAILABILITY control on
    // every push: the upload is refused.
    //
    // So the ceiling is not a guess, it is 100x a measurement. See
    // `MEASURED_FUEL_PEAK` above for the table and the load each number was
    // taken under. The multiple is deliberately absurd: what this bound exists
    // to stop is a guest that does not terminate, and such a guest burns 1e8 in
    // well under a second. It is not here to police an honest guest that got 3x
    // slower on a bigger repository, and at 100x it cannot.
    fuel: METER_FUEL_CEILING,
    // ⚠ NOT 250 ms. The deadline is the backstop for a guest parked inside a
    // host import — and a meter's imports are `host_usage_read` (which walks a
    // repository listing and reads archive metadata off disk) and
    // `host_store_list`. On a cold cache, on a big repository, on a loaded box,
    // that walk is the slow part and it is holger's own I/O, not the guest's
    // misbehaviour. Cutting the deadline to 250 ms would convert a slow disk
    // into a refused upload, which is the availability failure this ruling was
    // careful to avoid on the fuel axis; it would be perverse to reintroduce it
    // on the clock axis. The 30 s backstop stays, and it still ends a hang.
    call_timeout_ms: 30_000,
};

/// The Meter kind's fuel ceiling: [`MEASURED_FUEL_PEAK`] x
/// [`METER_FUEL_MULTIPLE`] — **100 000 000 instructions**, a tenth of the
/// package handler's 1e9 and 129x the open guest's measured worst call.
///
/// ⚠ WHAT THIS CEILING IMPLIES, said rather than discovered. `selftest` costs
/// ~753 fuel per key in the meter's own store, so this ceiling is reached at
/// roughly **130 000 keys** in `<data_root>/meters`. That directory is not a
/// repository: it is the meter plane's private store, the open guest holds only
/// `store.list` over it and cannot write a byte into it, and nothing in holger
/// puts files there. A deployment that fills it past six figures by hand gets a
/// NAMED fuel refusal on `selftest` — not a hang, not a wrong number — and this
/// paragraph is the answer to why.
///
/// The PUT path is nowhere near any of this: `evaluate` measures 2 636, which is
/// 38 000x under the ceiling.
pub const METER_FUEL_CEILING: u64 = MEASURED_FUEL_PEAK * METER_FUEL_MULTIPLE;

/// How far above the measured peak the Meter ceiling sits. The ruling asked for
/// at least 100x, for the reason spelled out on [`METER_SPEC`].
pub const METER_FUEL_MULTIPLE: u64 = 100;

/// Every kind this host speaks. The loader's only table.
pub static KIND_SPECS: &[&KindSpec] = &[&PACKAGE_HANDLER_SPEC, &METER_SPEC];

impl GuestKind {
    pub fn spec(self) -> &'static KindSpec {
        KIND_SPECS
            .iter()
            .copied()
            .find(|s| s.kind == self)
            .expect("every GuestKind variant has a KindSpec — asserted by a test")
    }

    pub fn name(self) -> &'static str {
        self.spec().name
    }

    pub fn mandatory(self) -> &'static [Capability] {
        self.spec().mandatory
    }

    /// This kind's CPU-fuel ceiling for one call. See [`KindSpec::fuel`]: the
    /// host takes the tighter of this and the operator's configured fuel, so a
    /// deployment can only narrow it.
    pub fn fuel(self) -> u64 {
        self.spec().fuel
    }

    /// This kind's wall-clock ceiling for one call, in milliseconds, applied the
    /// same way.
    pub fn call_timeout_ms(self) -> u64 {
        self.spec().call_timeout_ms
    }

    pub fn ops(self) -> &'static [OpSpec] {
        self.spec().ops
    }

    /// An unknown kind is refused naming the kinds this host speaks — the same
    /// by-name refusal shape as everything else here.
    pub fn from_u32(v: u32) -> Result<Self, String> {
        KIND_SPECS
            .iter()
            .copied()
            .find(|s| s.kind as u32 == v)
            .map(|s| s.kind)
            .ok_or_else(|| {
                format!(
                    "unknown guest kind {v}; this holger speaks: {}",
                    KIND_SPECS
                        .iter()
                        .map(|s| s.name)
                        .collect::<Vec<_>>()
                        .join(", ")
                )
            })
    }
}

// ───────────────────────────── the TLV manifest ─────────────────────────────

/// Tag numbering: the HIGH BIT marks REQUIRED.
///
/// * `0x0001..=0x7FFF` optional — an unknown one is skipped and **remembered**.
/// * `0x8001..=0xFFFF` required — an unknown one is **refused by tag**.
pub const REQUIRED_BIT: u16 = 0x8000;

pub mod tags {
    use super::REQUIRED_BIT;
    pub const ABI: u16 = REQUIRED_BIT | 0x0001;
    pub const KIND: u16 = REQUIRED_BIT | 0x0002;
    pub const NAME: u16 = REQUIRED_BIT | 0x0003;
    pub const REQUIRES: u16 = REQUIRED_BIT | 0x0004;
    pub const HANDLER: u16 = 0x0010;
    pub const METER: u16 = 0x0011;
    /// `(supplier, product, version)` — the anti-rollback floor's lane and the
    /// number it compares. OPTIONAL on the wire so a guest built before the
    /// floor still decodes; a host that KEEPS a floor refuses its absence by
    /// name (`holger-plugin-12`).
    pub const IDENTITY: u16 = 0x0012;
}

/// What a PackageHandler guest says about itself, beyond the common fields.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HandlerFacts {
    /// The `holger_traits::ArtifactFormat` spelling (`znippy`, `maven3`, …).
    pub format: String,
    pub writable: bool,
}

/// What a Meter guest says about itself, beyond the common fields.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct MeterFacts {
    /// Whether `accept_fact` is answered at all. The open guest says `false` and
    /// declines the op; nothing has to infer that from an error string.
    pub accepts_facts: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireManifest2 {
    pub abi: AbiRange,
    pub kind: GuestKind,
    pub name: String,
    /// Absent means "none" — a module that asks for nothing is legal and gets
    /// nothing.
    pub requires: Vec<Capability>,
    pub handler: Option<HandlerFacts>,
    pub meter: Option<MeterFacts>,
    /// Who this module is for the anti-rollback floor: `tags::IDENTITY`.
    pub identity: Option<crate::wire::ModuleIdentity>,
    /// Optional tags this host did not know. Kept, never interpreted, printed by
    /// `holger plugin describe` so an operator can see the module said MORE than
    /// this holger understands.
    pub unknown_optional: Vec<(u16, Vec<u8>)>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManifestError {
    /// The refusal that makes forward tolerance safe: a REQUIRED tag this host
    /// has never heard of means the module is relying on something this holger
    /// does not do.
    UnknownRequiredTag { tag: u16, host_abi_max: u32 },
    /// A `meter` block on a package handler, or the reverse.
    WrongKindPayload { kind: GuestKind, tag: u16 },
    /// A required tag the module simply did not write.
    MissingRequiredTag { tag: u16, name: &'static str },
    /// The kind number is not one this host speaks.
    UnknownKind { message: String },
    /// The bytes inside one tag did not decode.
    Decode { tag: u16, err: DecodeError },
}

impl core::fmt::Display for ManifestError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            ManifestError::UnknownRequiredTag { tag, host_abi_max } => write!(
                f,
                "plugin manifest carries REQUIRED tag 0x{tag:04x}, which this holger does not \
                 understand (it speaks ABI up to {host_abi_max}). An optional tag would have \
                 been carried; a required one cannot be."
            ),
            ManifestError::WrongKindPayload { kind, tag } => write!(
                f,
                "plugin manifest declares kind `{}` but carries tag 0x{tag:04x}, which belongs \
                 to a different kind",
                kind.name()
            ),
            ManifestError::MissingRequiredTag { tag, name } => {
                write!(
                    f,
                    "plugin manifest omits required tag 0x{tag:04x} (`{name}`)"
                )
            }
            ManifestError::UnknownKind { message } => write!(f, "{message}"),
            ManifestError::Decode { tag, err } => {
                write!(f, "plugin manifest tag 0x{tag:04x}: {err}")
            }
        }
    }
}

impl WireManifest2 {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut out = Writer::new();
        let put = |w: &mut Writer, tag: u16, body: Vec<u8>| {
            w.u16(tag).bytes(&body);
        };

        let mut b = Writer::new();
        self.abi.encode(&mut b);
        put(&mut out, tags::ABI, b.finish());

        let mut b = Writer::new();
        b.u32(self.kind as u32);
        put(&mut out, tags::KIND, b.finish());

        let mut b = Writer::new();
        b.str(&self.name);
        put(&mut out, tags::NAME, b.finish());

        let mut b = Writer::new();
        b.u32(self.requires.len() as u32);
        for c in &self.requires {
            c.encode(&mut b);
        }
        put(&mut out, tags::REQUIRES, b.finish());

        if let Some(h) = &self.handler {
            let mut b = Writer::new();
            b.str(&h.format).bool(h.writable);
            put(&mut out, tags::HANDLER, b.finish());
        }
        if let Some(m) = &self.meter {
            let mut b = Writer::new();
            b.bool(m.accepts_facts);
            put(&mut out, tags::METER, b.finish());
        }
        if let Some(id) = &self.identity {
            let mut b = Writer::new();
            id.encode(&mut b);
            put(&mut out, tags::IDENTITY, b.finish());
        }
        for (tag, body) in &self.unknown_optional {
            put(&mut out, *tag, body.clone());
        }
        out.finish()
    }

    /// Decode, tolerating unknown OPTIONAL tags and refusing unknown REQUIRED
    /// ones by tag number.
    ///
    /// Note what is NOT called: `expect_end`. A TLV stream ends when the bytes
    /// end, which is exactly the forward direction v1 does not have. The call
    /// payloads keep `expect_end` — the negotiated version already tells both
    /// sides which shape a payload has, so paying TLV cost on the hot path would
    /// buy nothing.
    pub fn from_bytes(buf: &[u8]) -> Result<Self, ManifestError> {
        let mut r = Reader::new(buf);
        let mut abi: Option<AbiRange> = None;
        let mut kind_raw: Option<u32> = None;
        let mut name: Option<String> = None;
        let mut requires: Vec<Capability> = Vec::new();
        let mut saw_requires = false;
        let mut handler_body: Option<Vec<u8>> = None;
        let mut meter_body: Option<Vec<u8>> = None;
        let mut identity: Option<crate::wire::ModuleIdentity> = None;
        let mut unknown_optional: Vec<(u16, Vec<u8>)> = Vec::new();

        while r.remaining() > 0 {
            let tag = r
                .u16("Manifest2.tag")
                .map_err(|err| ManifestError::Decode { tag: 0, err })?;
            let body = r
                .bytes("Manifest2.body")
                .map_err(|err| ManifestError::Decode { tag, err })?;
            let dec = |err: DecodeError| ManifestError::Decode { tag, err };
            match tag {
                tags::ABI => {
                    let mut br = Reader::new(&body);
                    abi = Some(AbiRange::decode(&mut br).map_err(dec)?);
                }
                tags::KIND => {
                    let mut br = Reader::new(&body);
                    kind_raw = Some(br.u32("Manifest2.kind").map_err(dec)?);
                }
                tags::NAME => {
                    let mut br = Reader::new(&body);
                    name = Some(br.str("Manifest2.name").map_err(dec)?);
                }
                tags::REQUIRES => {
                    saw_requires = true;
                    let mut br = Reader::new(&body);
                    let n = br.count("Manifest2.requires").map_err(dec)?;
                    for _ in 0..n {
                        requires.push(Capability::decode(&mut br).map_err(dec)?);
                    }
                }
                tags::HANDLER => handler_body = Some(body),
                tags::METER => meter_body = Some(body),
                tags::IDENTITY => {
                    let mut br = Reader::new(&body);
                    identity = Some(crate::wire::ModuleIdentity::decode(&mut br).map_err(dec)?);
                }
                t if t & REQUIRED_BIT != 0 => {
                    return Err(ManifestError::UnknownRequiredTag {
                        tag: t,
                        host_abi_max: HOST_ABI_MAX,
                    })
                }
                t => unknown_optional.push((t, body)),
            }
        }

        let abi = abi.ok_or(ManifestError::MissingRequiredTag {
            tag: tags::ABI,
            name: "abi",
        })?;
        let kind_raw = kind_raw.ok_or(ManifestError::MissingRequiredTag {
            tag: tags::KIND,
            name: "kind",
        })?;
        let name = name.ok_or(ManifestError::MissingRequiredTag {
            tag: tags::NAME,
            name: "name",
        })?;
        if !saw_requires {
            return Err(ManifestError::MissingRequiredTag {
                tag: tags::REQUIRES,
                name: "requires",
            });
        }
        let kind = GuestKind::from_u32(kind_raw)
            .map_err(|message| ManifestError::UnknownKind { message })?;

        let handler = match handler_body {
            None => None,
            Some(body) => {
                if kind != GuestKind::PackageHandler {
                    return Err(ManifestError::WrongKindPayload {
                        kind,
                        tag: tags::HANDLER,
                    });
                }
                let mut br = Reader::new(&body);
                let f = HandlerFacts {
                    format: br
                        .str("HandlerFacts.format")
                        .map_err(|err| ManifestError::Decode {
                            tag: tags::HANDLER,
                            err,
                        })?,
                    writable: br.bool("HandlerFacts.writable").map_err(|err| {
                        ManifestError::Decode {
                            tag: tags::HANDLER,
                            err,
                        }
                    })?,
                };
                Some(f)
            }
        };
        let meter = match meter_body {
            None => None,
            Some(body) => {
                if kind != GuestKind::Meter {
                    return Err(ManifestError::WrongKindPayload {
                        kind,
                        tag: tags::METER,
                    });
                }
                let mut br = Reader::new(&body);
                Some(MeterFacts {
                    accepts_facts: br.bool("MeterFacts.accepts_facts").map_err(|err| {
                        ManifestError::Decode {
                            tag: tags::METER,
                            err,
                        }
                    })?,
                })
            }
        };

        Ok(Self {
            abi,
            kind,
            name,
            requires,
            handler,
            meter,
            identity,
            unknown_optional,
        })
    }
}

// ───────────────────────────── grants ─────────────────────────────

/// Why a module was refused at load. Every variant names the module and the
/// thing, because a capability refusal an operator cannot act on is a boot
/// failure with no fix.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GrantError {
    /// Gate A: the manifest asked for a capability the operator did not grant.
    NotGranted {
        module: String,
        capability: &'static str,
        granted: Vec<&'static str>,
    },
    /// Gate B: the module IMPORTS something its manifest did not declare — a
    /// lying manifest.
    UndeclaredImport { module: String, import: String },
    /// The module imports something outside the whole capability vocabulary.
    UnknownImport { module: String, import: String },
    /// The kind requires a capability the module did not ask for.
    KindRequires {
        module: String,
        kind: &'static str,
        capability: &'static str,
    },
}

impl core::fmt::Display for GrantError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            GrantError::NotGranted {
                module,
                capability,
                granted,
            } => write!(
                f,
                "{module}: the module declares capability `{capability}`, which this \
                 deployment did not grant it. Granted: [{}]. Grants are operator \
                 configuration; a module dropped in without one can do nothing at all.",
                granted.join(", ")
            ),
            GrantError::UndeclaredImport { module, import } => write!(
                f,
                "{module}: the module imports `env::{import}` but its manifest does not \
                 declare the capability that unlocks it — the manifest does not describe \
                 the module"
            ),
            GrantError::UnknownImport { module, import } => write!(
                f,
                "{module}: the module imports `env::{import}`, which is not a holger host \
                 function at all"
            ),
            GrantError::KindRequires {
                module,
                kind,
                capability,
            } => write!(
                f,
                "{module}: a `{kind}` guest cannot function without `{capability}`, and this \
                 module's manifest does not declare it"
            ),
        }
    }
}

/// Check a module's manifest and real import list against an operator grant.
///
/// Both gates, in order, both refusing by name. Neither alone is enough: gate A
/// catches a module asking for more than it was given, gate B catches a module
/// that lies in its manifest and reaches for an import anyway.
///
/// The caller then defines **only** the returned import names on the linker, so
/// an ungranted capability is not merely refused — it is not linkable.
pub fn check_grants(
    module: &str,
    manifest: &WireManifest2,
    real_imports: &[String],
    granted: &[Capability],
) -> Result<Vec<&'static str>, GrantError> {
    // Gate A — declared vs granted.
    for want in &manifest.requires {
        if !granted.contains(want) {
            return Err(GrantError::NotGranted {
                module: module.to_string(),
                capability: want.name(),
                granted: granted.iter().map(|c| c.name()).collect(),
            });
        }
    }
    // The kind's floor.
    for need in manifest.kind.mandatory() {
        if !manifest.requires.contains(need) {
            return Err(GrantError::KindRequires {
                module: module.to_string(),
                kind: manifest.kind.name(),
                capability: need.name(),
            });
        }
    }
    // Gate B — imported vs declared.
    let unlocked: Vec<&'static str> = manifest
        .requires
        .iter()
        .flat_map(|c| c.imports().iter().copied())
        .collect();
    for imp in real_imports {
        match Capability::for_import(imp) {
            None => {
                return Err(GrantError::UnknownImport {
                    module: module.to_string(),
                    import: imp.clone(),
                })
            }
            Some(_) if !unlocked.contains(&imp.as_str()) => {
                return Err(GrantError::UndeclaredImport {
                    module: module.to_string(),
                    import: imp.clone(),
                })
            }
            Some(_) => {}
        }
    }
    Ok(unlocked)
}

// ───────────────────────────── the call context ─────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireVerb {
    Fetch = 0,
    Put = 1,
    List = 2,
    Http = 3,
    Delete = 4,
    Promote = 5,
}

impl WireVerb {
    pub fn name(self) -> &'static str {
        match self {
            WireVerb::Fetch => "fetch",
            WireVerb::Put => "put",
            WireVerb::List => "list",
            WireVerb::Http => "http",
            WireVerb::Delete => "delete",
            WireVerb::Promote => "promote",
        }
    }
    pub fn from_u8(v: u8) -> Result<Self, DecodeError> {
        Ok(match v {
            0 => WireVerb::Fetch,
            1 => WireVerb::Put,
            2 => WireVerb::List,
            3 => WireVerb::Http,
            4 => WireVerb::Delete,
            5 => WireVerb::Promote,
            tag => {
                return Err(DecodeError::BadTag {
                    field: "WireVerb",
                    tag,
                })
            }
        })
    }
}

/// Who is calling. `ident` is **exactly** `AuditEvent::ident` — the OIDC `sub`,
/// the mTLS CN, or `"anonymous"`. ONE spelling of identity in holger.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WirePrincipal {
    pub ident: String,
    /// `bearer` | `mtls` | `oidc` | `passkey` | `anonymous`
    pub method: String,
    /// `ip:port` as the server saw it; empty when the transport exposed none.
    pub source_ip: String,
}

impl WirePrincipal {
    pub fn encode(&self, w: &mut Writer) {
        w.str(&self.ident).str(&self.method).str(&self.source_ip);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            ident: r.str("Principal.ident")?,
            method: r.str("Principal.method")?,
            source_ip: r.str("Principal.source_ip")?,
        })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestAlgo {
    Blake3_256 = 0,
    Sha256 = 1,
}

impl DigestAlgo {
    pub fn name(self) -> &'static str {
        match self {
            DigestAlgo::Blake3_256 => "blake3-256",
            DigestAlgo::Sha256 => "sha256",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireDigest {
    pub algo: DigestAlgo,
    pub bytes: Vec<u8>,
}

/// **Two different reasons for an absent digest, kept distinguishable.** A guest
/// that gets `NotKnown` must be able to refuse rather than believe an absent
/// digest is a fresh one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestState {
    Present = 0,
    /// `digest.read` was not granted to this module.
    NotGranted = 1,
    /// The backend cannot produce a digest for this artifact.
    NotKnown = 2,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireArtifactRef {
    pub id: crate::wire::WireArtifactId,
    pub digest: Option<WireDigest>,
    pub digest_state: DigestState,
    pub size_bytes: u64,
}

impl WireArtifactRef {
    pub fn encode(&self, w: &mut Writer) {
        self.id.encode(w);
        match &self.digest {
            None => {
                w.u8(0);
            }
            Some(d) => {
                w.u8(1).u8(d.algo as u8).bytes(&d.bytes);
            }
        }
        w.u8(self.digest_state as u8).u64(self.size_bytes);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let id = crate::wire::WireArtifactId::decode(r)?;
        let digest = match r.u8("ArtifactRef.digest")? {
            0 => None,
            1 => {
                let algo = match r.u8("ArtifactRef.algo")? {
                    0 => DigestAlgo::Blake3_256,
                    1 => DigestAlgo::Sha256,
                    tag => {
                        return Err(DecodeError::BadTag {
                            field: "ArtifactRef.algo",
                            tag,
                        })
                    }
                };
                Some(WireDigest {
                    algo,
                    bytes: r.bytes("ArtifactRef.digest")?,
                })
            }
            tag => {
                return Err(DecodeError::BadTag {
                    field: "ArtifactRef.digest",
                    tag,
                })
            }
        };
        let digest_state = match r.u8("ArtifactRef.digest_state")? {
            0 => DigestState::Present,
            1 => DigestState::NotGranted,
            2 => DigestState::NotKnown,
            tag => {
                return Err(DecodeError::BadTag {
                    field: "ArtifactRef.digest_state",
                    tag,
                })
            }
        };
        Ok(Self {
            id,
            digest,
            digest_state,
            size_bytes: r.u64("ArtifactRef.size_bytes")?,
        })
    }
}

/// Where a time reading came from. An expiry decision taken on [`ClockSource::HostWall`]
/// alone is a decision the operator can move with a clock — gunnar measured VMs
/// booting +2h. The ABI cannot stop a guest doing that; it makes the choice
/// visible to whoever reads the guest, and the host logs which source it handed over.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClockSource {
    HostWall = 0,
    /// Time carried INSIDE a signed artefact the host verified.
    SignedFact = 1,
    /// Monotonic nanoseconds since this process started. Cannot be moved
    /// backwards by an operator with a clock.
    MonotonicSinceBoot = 2,
}

impl ClockSource {
    pub fn name(self) -> &'static str {
        match self {
            ClockSource::HostWall => "host-wall",
            ClockSource::SignedFact => "signed-fact",
            ClockSource::MonotonicSinceBoot => "monotonic-since-boot",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireInstant {
    pub unix_nanos: i64,
    pub source: ClockSource,
    /// How far into the future the host vouches for this reading. `0` = "no
    /// horizon, do not extrapolate".
    pub trusted_until_unix_nanos: i64,
}

impl WireInstant {
    pub fn encode(&self, w: &mut Writer) {
        w.i64(self.unix_nanos)
            .u8(self.source as u8)
            .i64(self.trusted_until_unix_nanos);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let unix_nanos = r.i64("Instant.unix_nanos")?;
        let source = match r.u8("Instant.source")? {
            0 => ClockSource::HostWall,
            1 => ClockSource::SignedFact,
            2 => ClockSource::MonotonicSinceBoot,
            tag => {
                return Err(DecodeError::BadTag {
                    field: "Instant.source",
                    tag,
                })
            }
        };
        Ok(Self {
            unix_nanos,
            source,
            trusted_until_unix_nanos: r.i64("Instant.trusted_until")?,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireCallContext {
    /// The negotiated ABI, so the guest never guesses the payload shape.
    pub abi: u32,
    /// holger's tenant unit: the REPOSITORY. holger has no namespaces and no
    /// accounts.
    pub repository: String,
    pub verb: WireVerb,
    /// `None` unless `principal.read` was granted. NOT an empty string — a guest
    /// must be able to tell "not granted" from "anonymous".
    pub principal: Option<WirePrincipal>,
    pub artifact: Option<WireArtifactRef>,
    /// `None` unless `clock.read` was granted.
    pub now: Option<WireInstant>,
}

impl WireCallContext {
    pub fn encode(&self, w: &mut Writer) {
        w.u32(self.abi).str(&self.repository).u8(self.verb as u8);
        match &self.principal {
            None => {
                w.u8(0);
            }
            Some(p) => {
                w.u8(1);
                p.encode(w);
            }
        }
        match &self.artifact {
            None => {
                w.u8(0);
            }
            Some(a) => {
                w.u8(1);
                a.encode(w);
            }
        }
        match &self.now {
            None => {
                w.u8(0);
            }
            Some(t) => {
                w.u8(1);
                t.encode(w);
            }
        }
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let abi = r.u32("Ctx.abi")?;
        let repository = r.str("Ctx.repository")?;
        let verb = WireVerb::from_u8(r.u8("Ctx.verb")?)?;
        let principal = match r.u8("Ctx.principal")? {
            0 => None,
            1 => Some(WirePrincipal::decode(r)?),
            tag => {
                return Err(DecodeError::BadTag {
                    field: "Ctx.principal",
                    tag,
                })
            }
        };
        let artifact = match r.u8("Ctx.artifact")? {
            0 => None,
            1 => Some(WireArtifactRef::decode(r)?),
            tag => {
                return Err(DecodeError::BadTag {
                    field: "Ctx.artifact",
                    tag,
                })
            }
        };
        let now = match r.u8("Ctx.now")? {
            0 => None,
            1 => Some(WireInstant::decode(r)?),
            tag => {
                return Err(DecodeError::BadTag {
                    field: "Ctx.now",
                    tag,
                })
            }
        };
        Ok(Self {
            abi,
            repository,
            verb,
            principal,
            artifact,
            now,
        })
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        self.encode(&mut w);
        w.finish()
    }

    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self::decode(&mut r)?;
        r.expect_end("WireCallContext")?;
        Ok(v)
    }
}

// ───────────────────────────── meters ─────────────────────────────

/// `monetize_product::Scope`'s words.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MeterScope {
    Tenant = 0,
    Product = 1,
}

/// `monetize_product::Enforcement`'s words. Only [`Enforcement::Enforced`] may
/// carry a cap — §8.2's rule, and the reason a cap is refused **at the sale**
/// rather than at the push.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Enforcement {
    Enforced = 0,
    Measured = 1,
    Provisioned = 2,
}

impl MeterScope {
    pub fn name(self) -> &'static str {
        match self {
            MeterScope::Tenant => "tenant",
            MeterScope::Product => "product",
        }
    }
    pub fn from_u8(v: u8) -> Result<Self, DecodeError> {
        Ok(match v {
            0 => MeterScope::Tenant,
            1 => MeterScope::Product,
            tag => {
                return Err(DecodeError::BadTag {
                    field: "MeterScope",
                    tag,
                })
            }
        })
    }
}

impl Enforcement {
    pub fn name(self) -> &'static str {
        match self {
            Enforcement::Enforced => "enforced",
            Enforcement::Measured => "measured",
            Enforcement::Provisioned => "provisioned",
        }
    }
    pub fn from_u8(v: u8) -> Result<Self, DecodeError> {
        Ok(match v {
            0 => Enforcement::Enforced,
            1 => Enforcement::Measured,
            2 => Enforcement::Provisioned,
            tag => {
                return Err(DecodeError::BadTag {
                    field: "Enforcement",
                    tag,
                })
            }
        })
    }
}

/// **THE honest-meter type.** There is no `u64` variant without a story.
///
/// `list(...).unwrap_or(0)` stamped with a repository scope is a confident FALSE
/// ZERO on the four backends that never override `list` — and it turns a broken
/// disk into an empty repository. This type exists so that reading cannot be
/// spelled.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadingValue {
    /// A number the backend actually counted.
    Counted(u64),
    /// The backend cannot answer this meter. Carries WHY, by name. This is NOT
    /// zero, and a fold that treats it as zero is the bug this type exists to
    /// make impossible.
    Refused { reason: String },
}

impl ReadingValue {
    /// The only way to get a number out, and it hands back an `Option` so the
    /// caller has to decide what a refusal means to it.
    pub fn counted(&self) -> Option<u64> {
        match self {
            ReadingValue::Counted(v) => Some(*v),
            ReadingValue::Refused { .. } => None,
        }
    }

    pub fn encode(&self, w: &mut Writer) {
        match self {
            ReadingValue::Counted(v) => {
                w.u8(0).u64(*v);
            }
            ReadingValue::Refused { reason } => {
                w.u8(1).str(reason);
            }
        }
    }

    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        match r.u8("ReadingValue")? {
            0 => Ok(ReadingValue::Counted(r.u64("ReadingValue.counted")?)),
            1 => Ok(ReadingValue::Refused {
                reason: r.str("ReadingValue.reason")?,
            }),
            tag => Err(DecodeError::BadTag {
                field: "ReadingValue",
                tag,
            }),
        }
    }
}

/// The meter names holger's host and holger's open guest both spell. One
/// writer, so a reading cannot be published under one name and looked up under
/// another.
pub mod meters {
    /// The repository argument that asks for the PRODUCT-scope readings rather
    /// than one tenant's. Empty, because holger's tenant unit is the repository
    /// and "no repository" is exactly what product scope means here.
    pub const PRODUCT_SCOPE: &str = "";

    /// Bytes held by the artifacts in one repository. The one meter every
    /// backend can answer, from `ArtifactEntry.size_bytes`.
    pub const ARTIFACT_BYTES: &str = "artifact_bytes";
    /// Artifacts held in one repository. REFUSED on a backend that does not
    /// override `list` — four of holger's do not.
    pub const ARTIFACTS: &str = "artifacts";
    pub const ARCHIVE_BYTES: &str = "archive_bytes";
    pub const ARCHIVE_FILES: &str = "archive_files";

    /// How much storage this holger has.
    pub const STORAGE_TOTAL_BYTES: &str = "storage_total_bytes";
    /// How much of it holger can still write into.
    pub const STORAGE_SERVABLE_BYTES: &str = "storage_servable_bytes";
    /// How much of it is in use.
    pub const STORAGE_USED_BYTES: &str = "storage_used_bytes";
}

/// What a meter IS, declared by the guest. The host fills nothing in here — but
/// it does refuse a cap sold against a meter whose `enforcement` is not
/// `Enforced`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireMeterDecl {
    pub meter: String,
    pub unit: String,
    pub scope: MeterScope,
    pub enforcement: Enforcement,
    pub description: String,
}

impl WireMeterDecl {
    pub fn encode(&self, w: &mut Writer) {
        w.str(&self.meter)
            .str(&self.unit)
            .u8(self.scope as u8)
            .u8(self.enforcement as u8)
            .str(&self.description);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            meter: r.str("MeterDecl.meter")?,
            unit: r.str("MeterDecl.unit")?,
            scope: MeterScope::from_u8(r.u8("MeterDecl.scope")?)?,
            enforcement: Enforcement::from_u8(r.u8("MeterDecl.enforcement")?)?,
            description: r.str("MeterDecl.description")?,
        })
    }
}

/// One reading. `scope` and `enforcement` are the HOST's to fill when it folds a
/// guest's readings, so a guest cannot upgrade its own meter to `Enforced`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireMeterReading {
    pub meter: String,
    pub scope: MeterScope,
    pub enforcement: Enforcement,
    pub value: ReadingValue,
}

impl WireMeterReading {
    pub fn encode(&self, w: &mut Writer) {
        w.str(&self.meter)
            .u8(self.scope as u8)
            .u8(self.enforcement as u8);
        self.value.encode(w);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            meter: r.str("MeterReading.meter")?,
            scope: MeterScope::from_u8(r.u8("MeterReading.scope")?)?,
            enforcement: Enforcement::from_u8(r.u8("MeterReading.enforcement")?)?,
            value: ReadingValue::decode(r)?,
        })
    }
}

/// How much storage there IS, how much is still servable, how much is used.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireCapacity {
    pub total_bytes: u64,
    /// What holger can actually still write — not `total - used`, which ignores
    /// a reservation, a quota on the mount, or a read-only filesystem.
    pub servable_bytes: u64,
    pub used_bytes: u64,
    /// `Refused` when the backend cannot say. The three numbers above are then
    /// not to be believed, and the reason says why.
    pub source: ReadingValue,
}

impl WireCapacity {
    pub fn encode(&self, w: &mut Writer) {
        w.u64(self.total_bytes)
            .u64(self.servable_bytes)
            .u64(self.used_bytes);
        self.source.encode(w);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            total_bytes: r.u64("Capacity.total_bytes")?,
            servable_bytes: r.u64("Capacity.servable_bytes")?,
            used_bytes: r.u64("Capacity.used_bytes")?,
            source: ReadingValue::decode(r)?,
        })
    }
}

/// The result of a check the guest actually ran. `detail` says what was measured
/// and what was expected, **in words**: on an appliance with no shell that is the
/// only way an operator sees the check ran at all.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireSelftest {
    pub name: String,
    pub passed: bool,
    pub detail: String,
}

impl WireSelftest {
    pub fn encode(&self, w: &mut Writer) {
        w.str(&self.name).bool(self.passed).str(&self.detail);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            name: r.str("Selftest.name")?,
            passed: r.bool("Selftest.passed")?,
            detail: r.str("Selftest.detail")?,
        })
    }
}

/// What a GUEST may say into the audit log.
///
/// Note what is absent: **no timestamp, no identity, no source address.** The
/// host stamps `ts_nanos`, `ident` and `source_ip` from the call it is already
/// inside, so a plugin can never write a record naming somebody else or claiming
/// another time.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WireAuditRecord {
    /// Free string. `AuditAction::from_wire` maps an unknown value to `Other`,
    /// so a newer plugin writing a newer verb into an older holger degrades
    /// instead of failing.
    pub action: String,
    pub repository: String,
    pub artifact: String,
    pub status: u16,
    pub bytes: u64,
    pub detail: String,
}

impl WireAuditRecord {
    pub fn encode(&self, w: &mut Writer) {
        w.str(&self.action)
            .str(&self.repository)
            .str(&self.artifact)
            .u16(self.status)
            .u64(self.bytes)
            .str(&self.detail);
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        Ok(Self {
            action: r.str("Audit.action")?,
            repository: r.str("Audit.repository")?,
            artifact: r.str("Audit.artifact")?,
            status: r.u16("Audit.status")?,
            bytes: r.u64("Audit.bytes")?,
            detail: r.str("Audit.detail")?,
        })
    }
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut w = Writer::new();
        self.encode(&mut w);
        w.finish()
    }
    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
        let mut r = Reader::new(b);
        let v = Self::decode(&mut r)?;
        r.expect_end("WireAuditRecord")?;
        Ok(v)
    }
}

/// What a gate-shaped guest answers. The guest **produces** a verdict; it does
/// not apply one — applying is a `Gate`'s job.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireVerdict {
    Allow,
    /// Allowed, with a line the client must SEE. Never hold data hostage; warn
    /// on the way through.
    AllowWithNotice {
        line: String,
    },
    /// Refused BEFORE the bytes. `code` is a holger-errcode name so the refusal
    /// is greppable and stable; `reason` is the human line; `url` is where to go.
    Refuse {
        code: String,
        reason: String,
        url: Option<String>,
    },
}

impl WireVerdict {
    pub fn encode(&self, w: &mut Writer) {
        match self {
            WireVerdict::Allow => {
                w.u8(0);
            }
            WireVerdict::AllowWithNotice { line } => {
                w.u8(1).str(line);
            }
            WireVerdict::Refuse { code, reason, url } => {
                w.u8(2).str(code).str(reason).opt_str(url.as_deref());
            }
        }
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        match r.u8("Verdict")? {
            0 => Ok(WireVerdict::Allow),
            1 => Ok(WireVerdict::AllowWithNotice {
                line: r.str("Verdict.line")?,
            }),
            2 => Ok(WireVerdict::Refuse {
                code: r.str("Verdict.code")?,
                reason: r.str("Verdict.reason")?,
                url: r.opt_str("Verdict.url")?,
            }),
            tag => Err(DecodeError::BadTag {
                field: "Verdict",
                tag,
            }),
        }
    }
}

/// What a guest reports back after it accepted (or refused) a signed fact. No
/// key material and no signature bytes cross back — the host keeps none of it.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WireFactSummary {
    pub repository: String,
    /// The guest's own word for what the fact says. Free text on purpose: holger
    /// has no vocabulary of plans and must not grow one.
    pub summary: String,
    /// `0` when the fact carries no expiry. Never "expired now".
    pub valid_until_unix_nanos: i64,
    /// Which clock source the guest compared against, echoed so an operator can
    /// see a decision taken on an untrusted wall clock.
    pub decided_against: Option<ClockSource>,
}

impl WireFactSummary {
    pub fn encode(&self, w: &mut Writer) {
        w.str(&self.repository)
            .str(&self.summary)
            .i64(self.valid_until_unix_nanos);
        match self.decided_against {
            None => {
                w.u8(0);
            }
            Some(s) => {
                w.u8(1).u8(s as u8);
            }
        }
    }
    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
        let repository = r.str("FactSummary.repository")?;
        let summary = r.str("FactSummary.summary")?;
        let valid_until_unix_nanos = r.i64("FactSummary.valid_until")?;
        let decided_against = match r.u8("FactSummary.decided_against")? {
            0 => None,
            1 => Some(match r.u8("FactSummary.source")? {
                0 => ClockSource::HostWall,
                1 => ClockSource::SignedFact,
                2 => ClockSource::MonotonicSinceBoot,
                tag => {
                    return Err(DecodeError::BadTag {
                        field: "FactSummary.source",
                        tag,
                    })
                }
            }),
            tag => {
                return Err(DecodeError::BadTag {
                    field: "FactSummary.decided_against",
                    tag,
                })
            }
        };
        Ok(Self {
            repository,
            summary,
            valid_until_unix_nanos,
            decided_against,
        })
    }
}

// ───────────────────────────── the guest-side trait ─────────────────────────────

/// The refusal a guest returns for an op it does not answer. Spelled once so the
/// host can tell "this guest declines this op" from "this guest broke".
pub const UNSUPPORTED: &str = "UNSUPPORTED";

/// What the host lends a Meter guest. **Every method defaults to a named
/// refusal**, so a guest that was not granted a capability gets an honest `Err`
/// rather than a stub that answers zero — and in the wasm case never even
/// reaches here, because the import is not defined on the linker.
pub trait MeterHost {
    fn store_get(&self, _key: &str) -> Result<Option<Vec<u8>>, String> {
        Err(not_granted(Capability::StoreRead))
    }
    fn store_put(&self, _key: &str, _data: &[u8]) -> Result<(), String> {
        Err(not_granted(Capability::StoreWrite))
    }
    fn store_list(&self, _prefix: &str) -> Result<Vec<(String, u64)>, String> {
        Err(not_granted(Capability::StoreList))
    }
    fn store_size(&self, _key: &str) -> Result<Option<u64>, String> {
        Err(not_granted(Capability::StoreRead))
    }
    fn usage_read(&self, _repository: &str) -> Result<Vec<WireMeterReading>, String> {
        Err(not_granted(Capability::UsageRead))
    }
    fn audit_write(&self, _record: &WireAuditRecord) -> Result<(), String> {
        Err(not_granted(Capability::AuditWrite))
    }
    fn clock(&self) -> Result<WireInstant, String> {
        Err(not_granted(Capability::ClockRead))
    }
}

fn not_granted(c: Capability) -> String {
    format!("capability `{}` is not granted to this module", c.name())
}

/// **The format-free half of a meter guest**, pure over a [`MeterHost`] — so the
/// one implementation compiles native and for `wasm32-unknown-unknown`, which is
/// the property `plugin/abi` was built to have.
pub trait Meter {
    fn manifest(&self) -> WireManifest2;

    /// What this guest measures, and under which enforcement. A cap may only be
    /// sold against a meter this declares `Enforced`.
    fn meters(&self) -> Result<Vec<WireMeterDecl>, String>;

    fn read(&self, host: &dyn MeterHost, repository: &str)
        -> Result<Vec<WireMeterReading>, String>;

    /// How much storage there IS and is servable.
    fn capacity(&self, host: &dyn MeterHost) -> Result<WireCapacity, String>;

    /// Run a check and say, in words, what was measured and what was expected.
    fn selftest(&self, host: &dyn MeterHost) -> Result<WireSelftest, String>;

    /// The verdict. **Total** for a guest that has no refusal to make.
    fn evaluate(&self, host: &dyn MeterHost, ctx: &WireCallContext) -> Result<WireVerdict, String>;

    /// Accept a signed fact. Declined with [`UNSUPPORTED`] by a guest that holds
    /// no key — which is not an error, and the manifest says so up front in
    /// [`MeterFacts::accepts_facts`].
    fn accept_fact(
        &self,
        _host: &dyn MeterHost,
        _signed: &[u8],
    ) -> Result<WireFactSummary, String> {
        Err(UNSUPPORTED.to_string())
    }

    /// Describe an installed fact without changing anything.
    fn describe_fact(
        &self,
        _host: &dyn MeterHost,
        _repository: &str,
    ) -> Result<WireFactSummary, String> {
        Err(UNSUPPORTED.to_string())
    }
}

// ───────────────────────────── payload encoders ─────────────────────────────

/// The `plugin_call` payload encoders, shared by host and guest so a field
/// cannot be written one way and read another.
pub mod payload {
    use super::*;
    use crate::codec::{read_result, write_result};

    pub fn encode_meters(v: &Result<Vec<WireMeterDecl>, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, rows| {
            w.u32(rows.len() as u32);
            for r in rows {
                r.encode(w);
            }
        });
        w.finish()
    }

    pub fn decode_meters(b: &[u8]) -> Result<Result<Vec<WireMeterDecl>, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "meters", |r| {
            let n = r.count("meters")?;
            (0..n).map(|_| WireMeterDecl::decode(r)).collect()
        })?;
        r.expect_end("meters")?;
        Ok(v)
    }

    pub fn encode_readings(v: &Result<Vec<WireMeterReading>, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, rows| {
            w.u32(rows.len() as u32);
            for r in rows {
                r.encode(w);
            }
        });
        w.finish()
    }

    pub fn decode_readings(b: &[u8]) -> Result<Result<Vec<WireMeterReading>, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "readings", |r| {
            let n = r.count("readings")?;
            (0..n).map(|_| WireMeterReading::decode(r)).collect()
        })?;
        r.expect_end("readings")?;
        Ok(v)
    }

    pub fn encode_capacity(v: &Result<WireCapacity, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, c| c.encode(w));
        w.finish()
    }

    pub fn decode_capacity(b: &[u8]) -> Result<Result<WireCapacity, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "capacity", WireCapacity::decode)?;
        r.expect_end("capacity")?;
        Ok(v)
    }

    pub fn encode_selftest(v: &Result<WireSelftest, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, s| s.encode(w));
        w.finish()
    }

    pub fn decode_selftest(b: &[u8]) -> Result<Result<WireSelftest, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "selftest", WireSelftest::decode)?;
        r.expect_end("selftest")?;
        Ok(v)
    }

    pub fn encode_verdict(v: &Result<WireVerdict, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, d| d.encode(w));
        w.finish()
    }

    pub fn decode_verdict(b: &[u8]) -> Result<Result<WireVerdict, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "verdict", WireVerdict::decode)?;
        r.expect_end("verdict")?;
        Ok(v)
    }

    pub fn encode_fact_summary(v: &Result<WireFactSummary, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, s| s.encode(w));
        w.finish()
    }

    pub fn decode_fact_summary(b: &[u8]) -> Result<Result<WireFactSummary, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "fact_summary", WireFactSummary::decode)?;
        r.expect_end("fact_summary")?;
        Ok(v)
    }

    /// `host_usage_read`'s answer.
    pub fn encode_usage(v: &Result<Vec<WireMeterReading>, String>) -> Vec<u8> {
        encode_readings(v)
    }

    pub fn decode_usage(b: &[u8]) -> Result<Result<Vec<WireMeterReading>, String>, DecodeError> {
        decode_readings(b)
    }

    /// `host_clock`'s answer.
    pub fn encode_clock(v: &Result<WireInstant, String>) -> Vec<u8> {
        let mut w = Writer::new();
        write_result(&mut w, v, |w, t| t.encode(w));
        w.finish()
    }

    pub fn decode_clock(b: &[u8]) -> Result<Result<WireInstant, String>, DecodeError> {
        let mut r = Reader::new(b);
        let v = read_result(&mut r, "clock", WireInstant::decode)?;
        r.expect_end("clock")?;
        Ok(v)
    }

    /// A bare length-prefixed string argument (`read`, `describe_fact`).
    pub fn encode_str_arg(s: &str) -> Vec<u8> {
        let mut w = Writer::new();
        w.str(s);
        w.finish()
    }

    pub fn decode_str_arg(b: &[u8]) -> Result<String, DecodeError> {
        let mut r = Reader::new(b);
        let s = r.str("arg")?;
        r.expect_end("arg")?;
        Ok(s)
    }

    /// A bare length-prefixed byte argument (`accept_fact`).
    pub fn encode_bytes_arg(b: &[u8]) -> Vec<u8> {
        let mut w = Writer::new();
        w.bytes(b);
        w.finish()
    }

    pub fn decode_bytes_arg(b: &[u8]) -> Result<Vec<u8>, DecodeError> {
        let mut r = Reader::new(b);
        let v = r.bytes("arg")?;
        r.expect_end("arg")?;
        Ok(v)
    }
}

/// Dispatch one op against a [`Meter`]. **Written once**, so the native runtime
/// and the wasm guest shim answer an op identically by construction rather than
/// by a guard watching two copies agree.
pub fn dispatch_meter(m: &dyn Meter, host: &dyn MeterHost, op: u32, arg: &[u8]) -> Vec<u8> {
    match op {
        ops::MANIFEST => m.manifest().to_bytes(),
        ops::METERS => payload::encode_meters(&m.meters()),
        ops::READ => {
            let out = match payload::decode_str_arg(arg) {
                Ok(repo) => m.read(host, &repo),
                Err(e) => Err(e.message()),
            };
            payload::encode_readings(&out)
        }
        ops::CAPACITY => payload::encode_capacity(&m.capacity(host)),
        ops::SELFTEST => payload::encode_selftest(&m.selftest(host)),
        ops::EVALUATE => {
            let out = match WireCallContext::from_bytes(arg) {
                Ok(ctx) => m.evaluate(host, &ctx),
                Err(e) => Err(e.message()),
            };
            payload::encode_verdict(&out)
        }
        ops::ACCEPT_FACT => {
            let out = match payload::decode_bytes_arg(arg) {
                Ok(bytes) => m.accept_fact(host, &bytes),
                Err(e) => Err(e.message()),
            };
            payload::encode_fact_summary(&out)
        }
        ops::DESCRIBE_FACT => {
            let out = match payload::decode_str_arg(arg) {
                Ok(repo) => m.describe_fact(host, &repo),
                Err(e) => Err(e.message()),
            };
            payload::encode_fact_summary(&out)
        }
        other => payload::encode_verdict(&Err(format!(
            "op {other} is not in the `meter` op space; this guest answers: {}",
            GuestKind::Meter
                .ops()
                .iter()
                .map(|o| format!("{} ({})", o.op, o.name))
                .collect::<Vec<_>>()
                .join(", ")
        ))),
    }
}

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

    #[test]
    fn every_kind_has_a_spec_and_the_loader_never_matches_on_one() {
        for k in [GuestKind::PackageHandler, GuestKind::Meter] {
            assert_eq!(k.spec().kind, k);
            assert_eq!(GuestKind::from_u32(k as u32).unwrap(), k);
        }
        let err = GuestKind::from_u32(3).unwrap_err();
        assert!(
            err.contains("package-handler") && err.contains("meter"),
            "{err}"
        );
    }

    #[test]
    fn negotiation_refuses_with_both_ranges_in_the_message() {
        assert_eq!(
            negotiate(AbiRange::new(1, 2), AbiRange::new(1, 1)).unwrap(),
            1
        );
        assert_eq!(
            negotiate(AbiRange::new(1, 2), AbiRange::new(2, 2)).unwrap(),
            2
        );
        assert_eq!(
            negotiate(AbiRange::new(1, 2), AbiRange::new(1, 9)).unwrap(),
            2
        );
        let err = negotiate(AbiRange::new(1, 2), AbiRange::new(5, 7)).unwrap_err();
        assert!(err.contains("1..=2") && err.contains("5..=7"), "{err}");
    }

    #[test]
    fn abi_range_packs_through_a_u64() {
        let r = AbiRange::new(1, 2);
        assert_eq!(AbiRange::unpack(r.pack()), r);
    }

    fn meter_manifest() -> WireManifest2 {
        WireManifest2 {
            abi: AbiRange::new(2, 2),
            kind: GuestKind::Meter,
            name: "allocation".into(),
            requires: vec![Capability::UsageRead, Capability::StoreList],
            handler: None,
            meter: Some(MeterFacts {
                accepts_facts: false,
            }),
            identity: Some(crate::wire::ModuleIdentity::new(
                "nordisk",
                "allocation",
                "0.1.0",
            )),
            unknown_optional: Vec::new(),
        }
    }

    #[test]
    fn a_manifest_round_trips() {
        let m = meter_manifest();
        assert_eq!(WireManifest2::from_bytes(&m.to_bytes()).unwrap(), m);
    }

    #[test]
    fn an_unknown_optional_tag_is_carried_and_an_unknown_required_tag_is_refused() {
        let mut m = meter_manifest();
        m.unknown_optional
            .push((0x0777, b"something newer".to_vec()));
        let decoded = WireManifest2::from_bytes(&m.to_bytes()).unwrap();
        assert_eq!(
            decoded.unknown_optional,
            vec![(0x0777u16, b"something newer".to_vec())]
        );

        // The same bytes with the high bit set are a refusal naming the tag.
        let mut bytes = meter_manifest().to_bytes();
        let mut w = Writer::new();
        w.u16(REQUIRED_BIT | 0x0777).bytes(b"something newer");
        bytes.extend_from_slice(&w.finish());
        match WireManifest2::from_bytes(&bytes) {
            Err(ManifestError::UnknownRequiredTag { tag, host_abi_max }) => {
                assert_eq!(tag, REQUIRED_BIT | 0x0777);
                assert_eq!(host_abi_max, HOST_ABI_MAX);
            }
            other => panic!("expected UnknownRequiredTag, got {other:?}"),
        }
    }

    #[test]
    fn a_meter_block_on_a_handler_is_refused_by_tag() {
        let mut m = meter_manifest();
        m.kind = GuestKind::PackageHandler;
        m.requires = vec![Capability::StoreRead, Capability::StoreWrite];
        match WireManifest2::from_bytes(&m.to_bytes()) {
            Err(ManifestError::WrongKindPayload { tag, .. }) => assert_eq!(tag, tags::METER),
            other => panic!("expected WrongKindPayload, got {other:?}"),
        }
    }

    #[test]
    fn capability_names_round_trip_and_cover_every_import() {
        for c in Capability::ALL {
            assert_eq!(Capability::from_name(c.name()), Some(*c));
        }
        for imp in imports::ALL_V2 {
            assert!(
                Capability::for_import(imp).is_some(),
                "no capability unlocks {imp}"
            );
        }
    }

    #[test]
    fn gate_a_refuses_an_ungranted_capability_by_name() {
        let m = meter_manifest();
        let err = check_grants("allocation.wasm", &m, &[], &[Capability::UsageRead]).unwrap_err();
        match &err {
            GrantError::NotGranted {
                capability,
                granted,
                ..
            } => {
                assert_eq!(*capability, "store.list");
                assert_eq!(granted, &["usage.read"]);
            }
            other => panic!("expected NotGranted, got {other:?}"),
        }
        assert!(err.to_string().contains("store.list"));
    }

    #[test]
    fn gate_b_refuses_a_lying_manifest_and_an_unknown_import() {
        let m = meter_manifest();
        let granted = [Capability::UsageRead, Capability::StoreList];
        // Imports audit.write without declaring it.
        match check_grants("x.wasm", &m, &[imports::AUDIT_WRITE.to_string()], &granted) {
            Err(GrantError::UndeclaredImport { import, .. }) => {
                assert_eq!(import, imports::AUDIT_WRITE)
            }
            other => panic!("expected UndeclaredImport, got {other:?}"),
        }
        match check_grants(
            "x.wasm",
            &m,
            &["host_launch_missiles".to_string()],
            &granted,
        ) {
            Err(GrantError::UnknownImport { import, .. }) => {
                assert_eq!(import, "host_launch_missiles")
            }
            other => panic!("expected UnknownImport, got {other:?}"),
        }
    }

    #[test]
    fn the_granted_import_set_is_what_the_linker_will_define() {
        let m = meter_manifest();
        let unlocked = check_grants(
            "allocation.wasm",
            &m,
            &[
                imports::USAGE_READ.to_string(),
                imports::STORE_LIST.to_string(),
            ],
            &[Capability::UsageRead, Capability::StoreList],
        )
        .unwrap();
        assert_eq!(unlocked, vec![imports::USAGE_READ, imports::STORE_LIST]);
        // The point of the whole gate: nothing else is in it.
        assert!(!unlocked.contains(&imports::AUDIT_WRITE));
        assert!(!unlocked.contains(&imports::STORE_PUT));
    }

    #[test]
    fn a_kind_floor_is_refused_by_name() {
        let mut m = meter_manifest();
        m.requires = vec![Capability::StoreList];
        match check_grants("x.wasm", &m, &[], &[Capability::StoreList]) {
            Err(GrantError::KindRequires {
                capability, kind, ..
            }) => {
                assert_eq!(capability, "usage.read");
                assert_eq!(kind, "meter");
            }
            other => panic!("expected KindRequires, got {other:?}"),
        }
    }

    #[test]
    fn an_uncounted_zero_cannot_be_spelled() {
        let refused = ReadingValue::Refused {
            reason: "znippy-go cannot enumerate".into(),
        };
        assert_eq!(refused.counted(), None);
        assert_ne!(refused, ReadingValue::Counted(0));
        let mut w = Writer::new();
        refused.encode(&mut w);
        let bytes = w.finish();
        let mut r = Reader::new(&bytes);
        assert_eq!(ReadingValue::decode(&mut r).unwrap(), refused);
    }

    #[test]
    fn the_context_round_trips_with_every_field_absent_and_present() {
        let bare = WireCallContext {
            abi: 2,
            repository: "releases".into(),
            verb: WireVerb::Put,
            principal: None,
            artifact: None,
            now: None,
        };
        assert_eq!(WireCallContext::from_bytes(&bare.to_bytes()).unwrap(), bare);

        let full = WireCallContext {
            abi: 2,
            repository: "releases".into(),
            verb: WireVerb::Fetch,
            principal: Some(WirePrincipal {
                ident: "anonymous".into(),
                method: "anonymous".into(),
                source_ip: String::new(),
            }),
            artifact: Some(WireArtifactRef {
                id: crate::wire::WireArtifactId {
                    namespace: None,
                    name: "a".into(),
                    version: "1".into(),
                },
                digest: Some(WireDigest {
                    algo: DigestAlgo::Blake3_256,
                    bytes: vec![1, 2, 3],
                }),
                digest_state: DigestState::Present,
                size_bytes: 7,
            }),
            now: Some(WireInstant {
                unix_nanos: 5,
                source: ClockSource::SignedFact,
                trusted_until_unix_nanos: 9,
            }),
        };
        assert_eq!(WireCallContext::from_bytes(&full.to_bytes()).unwrap(), full);
    }

    #[test]
    fn not_granted_and_not_known_are_different_absences() {
        assert_ne!(DigestState::NotGranted, DigestState::NotKnown);
    }

    #[test]
    fn an_audit_record_carries_no_time_no_identity_and_no_address() {
        // The compile-time half of the claim: the struct has six fields and none
        // of them is a clock or a name. The runtime half is the host stamping.
        let r = WireAuditRecord {
            action: "usage.read".into(),
            repository: "releases".into(),
            artifact: String::new(),
            status: 200,
            bytes: 0,
            detail: "artifact_bytes=17".into(),
        };
        assert_eq!(WireAuditRecord::from_bytes(&r.to_bytes()).unwrap(), r);
    }

    #[test]
    fn a_v1_module_gets_exactly_the_v1_bundle() {
        assert_eq!(
            Capability::V1_IMPLICIT,
            &[
                Capability::StoreRead,
                Capability::StoreWrite,
                Capability::StoreList
            ]
        );
        let unlocked: Vec<&str> = Capability::V1_IMPLICIT
            .iter()
            .flat_map(|c| c.imports().iter().copied())
            .collect();
        for imp in crate::imports::ALL {
            assert!(
                unlocked.contains(imp),
                "v1 import {imp} is not in the v1 bundle"
            );
        }
    }
}