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

nordisk/holger

plugin/host/src/lib.rs

//! **holger's package-handler plugin host.**
//!
//! Loads `.wasm` package handlers from a directory at startup and wraps each one
//! in a `RepositoryBackendTrait`, so `server/lib/src/exposed/fast_routes.rs`
//! cannot tell a linked handler from a loaded one — the route table holds
//! `Arc<dyn RepositoryBackendTrait>` and both kinds arrive as the same concrete
//! [`HandlerBackend`].
//!
//! ```text
//!  linked handler ──┐                                    ┌── plugin_fetch
//!                   ├─► HandlerRuntime ─► HandlerBackend ─┤
//!  loaded .wasm  ───┘        (one impl of RepositoryBackendTrait)
//! ```
//!
//! # Why the wasm side is behind a feature
//!
//! `wasm-plugins` (naming mirrors `znippy-common`'s) gates `dep:wasmtime`. With
//! the feature off this crate still builds and still hosts **linked** handlers —
//! so a server tier that wants the two bundle handlers compiled in, and no
//! wasmtime in its dependency graph, gets exactly that.
//!
//! # A loaded module is untrusted code, and is treated as such
//!
//! Two gates stand between a file in the plugin directory and a call on the
//! request path, and both were absent until 2026-09-19:
//!
//! * [`verify`] — a module is checked against a detached CMS signature and the
//!   operator's trusted roots BEFORE it is compiled. A module that cannot prove
//!   its provenance is a [`ScanError::UntrustedModule`] and stops the boot; it is
//!   never quietly dropped, because a catalog one handler short shows up in
//!   production as a 404 rather than as a stop at startup.
//! * [`limits`] — CPU fuel, a wall-clock deadline and a linear-memory ceiling,
//!   every one of them a NAMED refusal carrying a frozen `holger-plugin-<n>`
//!   code. The defaults are the safe ones and a configured `0` is refused, so a
//!   missing configuration cannot mean unbounded.
//!
//! [`ScanError::UntrustedModule`]: registry::ScanError::UntrustedModule
//!
//! # What is deliberately not here
//!
//! The loader's *shape* is borrowed from `znippy_common::plugins::wasm_loader` —
//! a fresh `Store` per call, `alloc`/`result_len`, host functions on a `Linker` —
//! but **no dependency edge is added for it**, because the owner was explicit that
//! holger touches znippy for the storage surface only.
//!
//! The ONE znippy edge this crate does have is `znippy-common/sign`, behind the
//! optional `verify-modules` feature, and it is a reuse edge rather than a
//! borrowed pattern: the constellation has exactly one spec-exact CMS/DSSE
//! verifier and holger already consumes it for `ron_trusted_roots`. A second
//! implementation of signature verification would be a second thing to get wrong.
//!
//! Two things about that loader were deliberately **not** copied:
//!
//! * its `parse_json_to_row` splits on `,` and `:` before stripping quotes and
//!   has no null representation, so a value containing a separator corrupts the
//!   row silently. The ABI here is length-prefixed instead, and
//!   `holger-plugin-abi/tests/codec_survives_what_json_did_not.rs` asserts the
//!   two properties that fixes.
//! * it is written in `.ok()?`, so a trap, a missing export and a genuinely
//!   absent value are all `None`. Here every failure is a named `Err`: a
//!   repository backend that reported a trapped module as "no such artifact"
//!   would be a silent 404 over bytes that exist.

/// The ABI, re-exported so a consumer of this host reaches ONE
/// `holger-plugin-abi` and cannot end up on a second copy through a different
/// version requirement. A server that named the ABI itself would be one
/// `version =` away from decoding a field it did not encode.
pub use holger_plugin_abi as abi;

pub mod backend;
pub mod catalog;
pub mod convert;
/// holger's frozen `holger-plugin-<n>` codes on `guest-host`'s typed refusals —
/// the seam that let the mechanics move to edda without the error registry.
pub mod named;
pub mod registry;
pub mod store;
/// Provenance: is a `.wasm` in the plugin directory the one we signed?
/// Compiled in EVERY build, wasm host or not — a build that cannot verify must
/// still be able to refuse a configured trust policy by name.
pub mod verify;
/// The lifted mechanics, re-exported so a consumer reaches ONE `guest-host`.
pub use guest_host;
/// The anti-rollback floor (W14): per `(supplier, product)`, the highest version
/// ever activated on this box. Pure Rust over the filesystem, compiled in EVERY
/// build — the policy that keeps one is named, and so is the one that does not.
pub mod floor;

/// Resource bounds. Only meaningful where there is a runtime to bound, so it
/// lives behind the same feature the runtime does.
#[cfg(feature = "wasm-plugins")]
pub mod limits;

#[cfg(feature = "wasm-plugins")]
pub mod wasm;

/// **The Meter kind.** `MeterRuntime` is `HandlerRuntime`'s sibling, with the
/// same two implementations; `GrantTable` is the operator's per-module decision,
/// read from a config file because the appliance has no shell.
///
/// Behind its own feature, so a build with `meter-guests` off carries no meter
/// op table, no verdict type and no capability vocabulary at all.
#[cfg(feature = "meter-guests")]
pub mod meter;

/// A meter guest loaded from a `.wasm`, whose linker defines ONLY the granted
/// imports.
#[cfg(all(feature = "meter-guests", feature = "wasm-plugins"))]
pub mod meter_wasm;

/// The meters this boot has, and the three censuses an operator (and the
/// open-build proof) can ask it for.
#[cfg(feature = "meter-guests")]
pub mod meter_catalog;

/// The native-vs-wasm agreement harness every handler pair's guard calls into.
/// Behind its own feature so the assertions are not compiled into a server
/// build, and available to a `-wasm` crate as a dev-dependency.
#[cfg(all(feature = "agreement-harness", feature = "wasm-plugins"))]
pub mod agreement;

pub use backend::{HandlerBackend, HandlerRuntime, LinkedHandler, SharedStore};
pub use catalog::{HandlerCatalog, HandlerError, Origin, RejectedModule};
pub use floor::{Admission, Floor, FloorPolicy, FloorStore};
pub use registry::{LoadFailure, Registry, ScanError};
pub use store::DirStore;
pub use verify::ModuleTrust;

#[cfg(feature = "wasm-plugins")]
pub use limits::{Bound, PluginLimits};

#[cfg(feature = "wasm-plugins")]
pub use wasm::WasmHandler;

#[cfg(feature = "meter-guests")]
pub use meter::{
    GrantTable, GrantedHost, LinkedMeter, MeterHostServices, MeterRuntime, ModuleCensusRow,
    NoServices,
};

#[cfg(all(feature = "meter-guests", feature = "wasm-plugins"))]
pub use meter_wasm::{MeterLoadError, WasmMeter};

#[cfg(feature = "meter-guests")]
pub use meter_catalog::{MeterCatalog, MeterScanOutcome};

/// **Everything a loaded module is held to**: what it may spend, and what it must
/// prove before it is compiled at all.
///
/// One type rather than two arguments, because the two halves are the same
/// decision — "this is untrusted code, running in the artifact server's process"
/// — and a call site that could pass limits without a trust policy would make
/// forgetting one of them the easy path.
///
/// [`Default`] is the SAFE answer for the half that has one. The limits default
/// to real ceilings ([`limits::PluginLimits::default`]), because a missing
/// configuration must never mean unbounded; trust defaults to
/// [`ModuleTrust::NotRequired`], because requiring signatures with no configured
/// roots could only refuse every module — trust turns on by naming roots, exactly
/// as `ron_trusted_roots` does for a znippy archive.
#[derive(Debug, Clone, Default)]
pub struct PluginPolicy {
    /// What one call may spend before it is refused by name.
    #[cfg(feature = "wasm-plugins")]
    pub limits: limits::PluginLimits,
    /// What a module must prove before it is compiled.
    pub trust: ModuleTrust,
    /// Whether this box keeps an anti-rollback floor, and where. A loaded module
    /// is placed against its `(supplier, product)` lane and refused by name
    /// below it; its activation raises the lane.
    pub floor: FloorPolicy,
}

impl PluginPolicy {
    /// The safe default policy: bounded execution, no provenance requirement.
    pub fn new() -> Self {
        Self::default()
    }

    /// Require every module to chain to `roots`.
    pub fn with_trust(mut self, trust: ModuleTrust) -> Self {
        self.trust = trust;
        self
    }

    /// Keep (or not) an anti-rollback floor under a directory.
    pub fn with_floor(mut self, floor: FloorPolicy) -> Self {
        self.floor = floor;
        self
    }

    /// Replace the resource bounds. Validated by the caller that installs them
    /// ([`wasm::WasmHandler::load_with`]), so a zero cannot slip through as
    /// "unlimited".
    #[cfg(feature = "wasm-plugins")]
    pub fn with_limits(mut self, limits: limits::PluginLimits) -> Self {
        self.limits = limits;
        self
    }
}

use std::path::Path;
use std::sync::Arc;

use holger_plugin_abi::{BlobStore, PackageHandler};

/// Mount a handler that is compiled into the server over a directory store.
///
/// The native half of "every package handler can be written native or WASM": the
/// same [`PackageHandler`] a `.wasm` is built from, called directly.
pub fn mount_linked<H: PackageHandler + Send + Sync + 'static>(
    repo_name: impl Into<String>,
    handler: H,
    data_dir: impl AsRef<Path>,
) -> Result<HandlerBackend, String> {
    let store: SharedStore = Arc::new(DirStore::new(data_dir.as_ref().to_path_buf()));
    HandlerBackend::new(repo_name, Box::new(LinkedHandler::new(handler, store)))
}

/// Mount a handler over a caller-supplied store (an in-memory one in tests, a
/// directory in production).
pub fn mount_linked_with<H: PackageHandler + Send + Sync + 'static>(
    repo_name: impl Into<String>,
    handler: H,
    store: SharedStore,
) -> Result<HandlerBackend, String> {
    HandlerBackend::new(repo_name, Box::new(LinkedHandler::new(handler, store)))
}

/// Wrap any `BlobStore` for use by a mounted handler.
pub fn shared_store(store: impl BlobStore + Send + Sync + 'static) -> SharedStore {
    Arc::new(store)
}