Luigit
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 {}