//! Wasm plugin host: deterministic engine, artifact validation, //! transactional load, and fuel with memory ceilings. //! //! The engine serves every plugin identically; embedded and user plugins //! load through this one path with no privilege difference. use ::smith::error::{ProviderFault, Result, SmithError}; use std::path::Path; use wasmtime::component::{Component, Linker}; use wasmtime::{Config, Engine, Store}; mod sealed_wasi; #[allow(missing_docs, reason = "generated bindings carry no doc comments")] pub mod bindings { // The host world: the guest-facing plugin world plus exactly the WASI // imports Rust std on wasm32-wasip2 pulls in, all served by Smith's // sealed implementations in `sealed_wasi`. wasmtime::component::bindgen!({ path: "../wit", inline: " package smith:host; world plugin-host { include smith:plugin/plugin-world@0.1.0; import wasi:io/error@0.2.12; import wasi:io/poll@0.2.12; import wasi:io/streams@0.2.12; import wasi:clocks/monotonic-clock@0.2.12; import wasi:cli/environment@0.2.12; import wasi:cli/exit@0.2.12; import wasi:cli/stdin@0.2.12; import wasi:cli/stdout@0.2.12; import wasi:cli/stderr@0.2.12; import wasi:cli/terminal-stdin@0.2.12; import wasi:cli/terminal-stdout@0.2.12; import wasi:cli/terminal-stderr@0.2.12; } ", imports: { "wasi:cli/exit.exit": trappable, "wasi:cli/exit.exit-with-code": trappable, }, }); } use self::bindings::PluginHost; use self::bindings::exports::smith::plugin::plugin::ToolDeclaration; use self::bindings::smith::plugin::tooling::Host as ToolingHost; /// Deterministic engine configuration per `SMH-SPEC-WASM0001`. pub struct PluginEngine { engine: Engine, } impl PluginEngine { /// Build the deterministic engine: NaN canonicalization on, fuel on, /// backtraces off. /// /// # Errors /// /// Returns a plugin fault when the engine cannot be configured. pub fn new() -> Result { let mut config = Config::new(); config.cranelift_nan_canonicalization(true); config.consume_fuel(true); config.wasm_backtrace_max_frames(std::num::NonZeroUsize::new(1)); let engine = Engine::new(&config).map_err(|e| plugin_fault(format!("engine config failed: {e}")))?; Ok(Self { engine }) } /// Validate one artifact against the world; a rejected artifact never /// instantiates. /// /// # Errors /// /// Returns a plugin fault naming the validation stage and cause. pub fn validate(&self, artifact: &Path) -> Result { Component::from_file(&self.engine, artifact) .map_err(|e| plugin_fault(format!("validation failed: {e}"))) } /// The shared engine behind every plugin. #[must_use] pub const fn engine(&self) -> &Engine { &self.engine } } /// One instantiated plugin: pinned store, fuel budget, and bindings. pub struct PluginInstance { store: Store, bindings: PluginHost, } /// Per-plugin runtime ceilings. #[derive(Clone, Copy, Debug)] pub struct PluginCeilings { /// Memory ceiling in bytes. pub memory_bytes: usize, /// Fuel budget per call. pub fuel: u64, } impl Default for PluginCeilings { fn default() -> Self { Self { memory_bytes: 64 * 1024 * 1024, fuel: 10_000_000_000, } } } struct PluginHostState { /// Host-injected monotonic clock reading; the guest never sees OS time. monotonic_now: u64, ceilings: PluginCeilings, } impl PluginHostState { fn limits(&mut self) -> &mut dyn wasmtime::ResourceLimiter { &mut self.ceilings } } impl wasmtime::ResourceLimiter for PluginCeilings { fn memory_growing( &mut self, _current: usize, desired: usize, _maximum: Option, ) -> wasmtime::Result { Ok(desired <= self.memory_bytes) } fn table_growing( &mut self, _current: usize, desired: usize, _maximum: Option, ) -> wasmtime::Result { Ok(desired <= 10_000) } } impl PluginInstance { /// Validate, instantiate, and pin one plugin atomically. /// /// # Errors /// /// Returns a plugin fault naming the failed lifecycle stage. pub fn load(engine: &PluginEngine, artifact: &Path) -> Result { let component = engine.validate(artifact)?; let ceilings = PluginCeilings::default(); let mut store = Store::new( engine.engine(), PluginHostState { monotonic_now: 0, ceilings, }, ); store.limiter(PluginHostState::limits); store .set_fuel(ceilings.fuel) .map_err(|e| plugin_fault(format!("fuel setup failed: {e}")))?; let mut linker: Linker = Linker::new(engine.engine()); PluginHost::add_to_linker::(&mut linker, |state| state) .map_err(|e| plugin_fault(format!("linker setup failed: {e}")))?; let bindings = PluginHost::instantiate(&mut store, &component, &linker) .map_err(|e| plugin_fault(format!("instantiation failed: {e}")))?; Ok(Self { store, bindings }) } /// The declared tool set; runs the declaration pass. /// /// # Errors /// /// Returns a plugin fault on trap, fuel exhaustion, or conversion /// failure. pub fn declare_tools(&mut self) -> Result> { let interface = self .bindings .smith_plugin_plugin() .call_declare_tools(&mut self.store) .map_err(|e| plugin_fault(format!("declaration failed: {e}")))?; Ok(interface) } /// Invoke one declared tool by name. /// /// # Errors /// /// Returns a plugin fault on trap, fuel exhaustion, or a guest error; /// guest errors carry the plugin's own message. pub fn invoke(&mut self, name: &str, input_json: &str) -> Result { let outcome = self .bindings .smith_plugin_plugin() .call_invoke(&mut self.store, name, input_json) .map_err(|e| plugin_fault(format!("invoke failed: {e}")))?; outcome.map_err(|message| plugin_fault(format!("plugin error: {message}"))) } } fn plugin_fault(message: impl Into) -> SmithError { SmithError::Provider { fault: ProviderFault::Plugin { message: message.into(), }, } } /// Self-referential data marker for linker setup. struct PluginHasSelf; impl wasmtime::component::HasData for PluginHasSelf { type Data<'a> = &'a mut PluginHostState; } impl ToolingHost for PluginHostState {}