Skip to main content

act_runtime/
engine.rs

1//! Engine construction, component loading, and the WASI linker.
2
3use anyhow::Result;
4use wasmtime::component::{Component, Linker};
5use wasmtime::{Config, Engine};
6
7use crate::store::HostState;
8use crate::{credentials, fs_policy};
9
10/// Create a wasmtime engine with component-model and async enabled.
11pub fn create_engine() -> Result<Engine> {
12    let mut config = Config::new();
13    config.wasm_component_model(true);
14    config.wasm_component_model_async(true);
15    // Enable wasm exception-handling so components carrying C++-exception
16    // extensions run (e.g. numpy 2.x's pocketfft throws). Additive: components
17    // without the exceptions proposal are unaffected.
18    config.wasm_exceptions(true);
19    // SPIKE: enable WasmGC so GC-backed guests (Kotlin/Wasm, future JVM/Dart) run.
20    config.wasm_function_references(true);
21    config.wasm_gc(true);
22    let engine = Engine::new(&config)
23        .map_err(|e| anyhow::anyhow!("failed to create wasmtime engine: {e}"))?;
24    Ok(engine)
25}
26/// Load a .wasm component from a file path and report the SHA-256 of its
27/// bytes.
28///
29/// The digest identifies the exact artifact in the audit trail, so it is read
30/// from the file rather than inferred from the reference — a local path and an
31/// OCI cache entry are treated identically.
32pub fn load_component(engine: &Engine, path: &std::path::Path) -> Result<(Component, String)> {
33    let bytes = std::fs::read(path)
34        .map_err(|e| anyhow::anyhow!("failed to read component {}: {e}", path.display()))?;
35    let digest = crate::audit::sha256_hex(&bytes);
36    let component = Component::from_binary(engine, &bytes)
37        .map_err(|e| anyhow::anyhow!("failed to load component from {}: {e}", path.display()))?;
38    Ok((component, digest))
39}
40/// Create a linker with WASI bindings (both P2 and P3).
41pub fn create_linker(engine: &Engine) -> Result<Linker<HostState>> {
42    let mut linker = Linker::new(engine);
43    // Add P2 bindings (components built with wasm32-wasip2 import P2 interfaces)
44    wasmtime_wasi::p2::add_to_linker_async(&mut linker)
45        .map_err(|e| anyhow::anyhow!("failed to add WASI P2 to linker: {e}"))?;
46    // Shadow the default wasi:filesystem bindings with our policy-aware
47    // PolicyFilesystem view. Must come AFTER add_to_linker_async registered
48    // the defaults.
49    linker.allow_shadowing(true);
50    wasmtime_wasi::p2::bindings::filesystem::types::add_to_linker::<
51        HostState,
52        fs_policy::PolicyFilesystem,
53    >(&mut linker, super::store::HostState::policy_fs_view)
54    .map_err(|e| anyhow::anyhow!("failed to add policy wasi:filesystem/types: {e}"))?;
55    wasmtime_wasi::p2::bindings::filesystem::preopens::add_to_linker::<
56        HostState,
57        fs_policy::PolicyFilesystem,
58    >(&mut linker, super::store::HostState::policy_fs_view)
59    .map_err(|e| anyhow::anyhow!("failed to add policy wasi:filesystem/preopens: {e}"))?;
60    linker.allow_shadowing(false);
61    // Add P3 bindings on top
62    wasmtime_wasi::p3::add_to_linker(&mut linker)
63        .map_err(|e| anyhow::anyhow!("failed to add WASI P3 to linker: {e}"))?;
64    // Shadow only the p3 preopens interface. When fs mode ≠ Open, our impl
65    // returns zero preopens → p3 guests can't obtain a Descriptor::Dir and
66    // every path op fails. Matcher-level gating on individual p3 path ops
67    // isn't possible with current wasmtime-wasi public API (Dir::open_at
68    // is `pub(crate)`).
69    linker.allow_shadowing(true);
70    wasmtime_wasi::p3::bindings::filesystem::preopens::add_to_linker::<
71        HostState,
72        fs_policy::PolicyFilesystem,
73    >(&mut linker, super::store::HostState::policy_fs_view)
74    .map_err(|e| anyhow::anyhow!("failed to add policy wasi:filesystem/preopens (p3): {e}"))?;
75    linker.allow_shadowing(false);
76    // Add WASI HTTP bindings (P2 for wasm32-wasip2 components, P3 for async)
77    wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)
78        .map_err(|e| anyhow::anyhow!("failed to add WASI HTTP P2 to linker: {e}"))?;
79    wasmtime_wasi_http::p3::add_to_linker(&mut linker)
80        .map_err(|e| anyhow::anyhow!("failed to add WASI HTTP P3 to linker: {e}"))?;
81    // `act:credentials` — the one interface in `act-world` the host provides
82    // and the component imports. Both its instances are registered; see
83    // `credentials::add_to_linker` for why `types` is not optional.
84    credentials::add_to_linker(&mut linker)?;
85    // `act:consent` — the second host-provided, component-imported interface.
86    // Registered unconditionally: a component that never imports it is
87    // unaffected, and one that does must find it or fail instantiation.
88    crate::consent::gate::add_to_linker(&mut linker)?;
89    Ok(linker)
90}