plugin/host/src/limits.rs
//! **What a loaded module may spend before the host refuses it, by name.**
//!
//! The limits themselves — fuel, the epoch deadline, the memory and table
//! ceilings, and the rule that a configured `0` is a misconfiguration and never
//! "unlimited" — are `guest_host::limits` (edda) since 2026-09-22, shared with
//! monetize's product host. This file keeps what is holger's: the frozen
//! `holger-plugin-<n>` code on each refusal, and the narrowing of an operator's
//! limits to a guest KIND's own ceilings (?17).
//!
//! Every default is the SAFE one; see `guest_host::limits` for the numbers and
//! the two-CPU-bounds argument.
use holger_errcode as ec;
pub use guest_host::limits::{
Bound, LimitZero, PluginLimits, DEFAULT_CALL_TIMEOUT, DEFAULT_FUEL, DEFAULT_MAX_MEMORY_BYTES,
DEFAULT_MAX_TABLE_ELEMENTS,
};
pub use guest_host::limits::{BoundedLimiter, EpochTicker};
/// Refuse a zero limit, naming the field, under `holger-plugin-5`.
pub fn validate(limits: &PluginLimits) -> Result<(), String> {
limits
.validate()
.map_err(|e| crate::named::limit_refusal(&e))
}
/// **Narrow limits to a guest KIND's own ceilings** — ?17, answered 2026-09-20.
///
/// The two CPU axes become the TIGHTER of what the operator configured and what
/// [`holger_plugin_abi::v2::KindSpec`] declares for the kind. Memory and tables
/// are untouched: they are about how much of the machine a module may hold,
/// which is a property of the box and not of the kind. It narrows and never
/// widens: nobody can raise a guest past the kind's ceiling by writing a bigger
/// number in a config file.
pub fn narrowed_to(limits: &PluginLimits, kind: holger_plugin_abi::v2::GuestKind) -> PluginLimits {
limits.narrowed(
kind.fuel(),
std::time::Duration::from_millis(kind.call_timeout_ms()),
)
}
/// The frozen code a [`Bound`] refusal is raised under.
pub fn bound_code(b: Bound) -> &'static ec::ErrCode {
crate::named::bound_code(b)
}
/// `[code] <path>: <export>: <what and how to fix it>`, quoting the limit hit.
pub fn named_refusal(b: Bound, path: &str, export: &str, limits: &PluginLimits) -> String {
bound_code(b).tag(b.refusal(path, export, limits))
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
/// The defaults are the SAFE ones and validate clean under holger's code.
#[test]
fn the_defaults_are_bounded() {
let l = PluginLimits::default();
validate(&l).expect("the defaults must be a valid configuration");
assert!(l.fuel > 0 && l.max_memory_bytes > 0 && l.max_table_elements > 0);
assert!(!l.call_timeout.is_zero());
}
/// Zero is refused, by name and under `holger-plugin-5`, for every field.
#[test]
fn a_zero_limit_is_refused_by_name_and_never_means_unlimited() {
for (field, limits) in [
(
"fuel",
PluginLimits {
fuel: 0,
..Default::default()
},
),
(
"max_memory_bytes",
PluginLimits {
max_memory_bytes: 0,
..Default::default()
},
),
(
"max_table_elements",
PluginLimits {
max_table_elements: 0,
..Default::default()
},
),
(
"call_timeout",
PluginLimits {
call_timeout: Duration::ZERO,
..Default::default()
},
),
] {
let err = validate(&limits).expect_err("a zero limit was accepted");
assert!(
err.contains(ec::PLUGIN_LIMIT_MISCONFIGURED.code),
"the refusal carries no code: {err}"
);
assert!(
err.contains(field),
"the refusal does not name the field {field}: {err}"
);
}
}
/// Every bound carries its own code — a caller cannot report one refusal as
/// another — and the coded refusal names module, export and limit.
#[test]
fn every_bound_has_its_own_code() {
let all = [Bound::Fuel, Bound::Deadline, Bound::Memory, Bound::Table];
let mut codes: Vec<&str> = all.iter().map(|b| bound_code(*b).code).collect();
codes.sort_unstable();
codes.dedup();
assert_eq!(codes.len(), all.len(), "two bounds share a code");
let limits = PluginLimits::default();
for b in all {
let msg = named_refusal(b, "/plugins/x.wasm", "plugin_fetch", &limits);
assert!(msg.contains(bound_code(b).code), "{msg}");
assert!(
msg.contains("plugin_fetch") && msg.contains("/plugins/x.wasm"),
"{msg}"
);
}
}
/// The kind's ceiling narrows an operator's numbers and never widens them.
#[test]
fn narrowing_to_a_kind_never_widens() {
use holger_plugin_abi::v2::GuestKind;
let generous = PluginLimits {
fuel: u64::MAX,
..Default::default()
};
assert_eq!(
narrowed_to(&generous, GuestKind::Meter).fuel,
GuestKind::Meter.fuel()
);
let tight = PluginLimits {
fuel: 1_000,
..Default::default()
};
assert_eq!(narrowed_to(&tight, GuestKind::Meter).fuel, 1_000);
}
}