repositories / smith
smith
There are many coding harnesses - but this one is fast
owned by admin
smith-harness/src/wasm.rs
Raw//! 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<Self> {
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> {
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<PluginHostState>,
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<usize>,
) -> wasmtime::Result<bool> {
Ok(desired <= self.memory_bytes)
}
fn table_growing(
&mut self,
_current: usize,
desired: usize,
_maximum: Option<usize>,
) -> wasmtime::Result<bool> {
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<Self> {
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<PluginHostState> = Linker::new(engine.engine());
PluginHost::add_to_linker::<PluginHostState, PluginHasSelf>(&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<Vec<ToolDeclaration>> {
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<String> {
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<String>) -> 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 {}