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