use async_trait::async_trait;
use serde_json::Value;
use wasmtime::{Config, Engine, Linker, Module, ResourceLimiter, Result as AnyhowResult, Store};
use wasmtime_wasi::WasiCtxBuilder;
use wasmtime_wasi::preview1::{self, WasiP1Ctx};
use crate::tool::{Capability, Tool, ToolDefinition};
use crate::tool_error::ToolError;
pub struct WasmTool {
definition: ToolDefinition,
module_bytes: Vec<u8>,
capabilities: Vec<Capability>,
memory_limit_bytes: usize,
fuel_limit: u64,
}
impl WasmTool {
pub fn new(
definition: ToolDefinition,
module_bytes: Vec<u8>,
capabilities: Vec<Capability>,
) -> Self {
if capabilities.contains(&Capability::Network)
|| capabilities.contains(&Capability::FileSystem)
{
tracing::warn!(
tool = %definition.name,
"Tool requests Network/FileSystem capabilities which are not enforced by WASI Preview 1"
);
}
Self {
definition,
module_bytes,
capabilities,
memory_limit_bytes: 64 * 1024 * 1024, fuel_limit: 10_000_000, }
}
pub fn with_memory_limit(mut self, limit: usize) -> Self {
self.memory_limit_bytes = limit;
self
}
pub fn with_fuel_limit(mut self, limit: u64) -> Self {
self.fuel_limit = limit;
self
}
}
struct WasmStoreData {
wasi: WasiP1Ctx,
memory_limit: usize,
table_elements_limit: u32,
}
impl ResourceLimiter for WasmStoreData {
fn memory_growing(
&mut self,
_current: usize,
desired: usize,
_maximum: Option<usize>,
) -> AnyhowResult<bool> {
if desired > self.memory_limit {
return Ok(false);
}
Ok(true)
}
fn table_growing(
&mut self,
_current: u32,
desired: u32,
_maximum: Option<u32>,
) -> AnyhowResult<bool> {
if desired > self.table_elements_limit {
return Ok(false);
}
Ok(true)
}
}
#[async_trait]
impl Tool for WasmTool {
fn definition(&self) -> &ToolDefinition {
&self.definition
}
fn capabilities(&self) -> Vec<Capability> {
self.capabilities.clone()
}
async fn execute(&self, args: Value) -> Result<Value, ToolError> {
let mut config = Config::new();
config.consume_fuel(true);
config.async_support(true);
config.wasm_bulk_memory(true);
config.wasm_multi_value(true);
config.wasm_reference_types(true);
let engine = Engine::new(&config).map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to create WASM engine: {}", e),
)
})?;
let module = Module::from_binary(&engine, &self.module_bytes).map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to load WASM module: {:?}", e),
)
})?;
let mut builder = WasiCtxBuilder::new();
builder.inherit_stdout().inherit_stderr();
if self.capabilities.contains(&Capability::Environment) {
builder.inherit_env();
}
if self.capabilities.contains(&Capability::Network) {
}
if self.capabilities.contains(&Capability::FileSystem) {
}
let wasi = builder.build_p1();
let table_elements_limit = 1000;
let mut store = Store::new(
&engine,
WasmStoreData {
wasi,
memory_limit: self.memory_limit_bytes,
table_elements_limit,
},
);
store.limiter(|s| s);
store.set_fuel(self.fuel_limit).map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to set WASM fuel: {}", e),
)
})?;
let mut linker = Linker::new(&engine);
preview1::add_to_linker_async(&mut linker, |s: &mut WasmStoreData| &mut s.wasi).map_err(
|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to link WASI: {}", e),
)
},
)?;
let instance = linker
.instantiate_async(&mut store, &module)
.await
.map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to instantiate WASM: {}", e),
)
})?;
let allocate = instance
.get_typed_func::<u32, u32>(&mut store, "vex_allocate")
.map_err(|_| {
ToolError::execution_failed(
self.definition.name,
"WASM module must export 'vex_allocate(u32) -> u32'",
)
})?;
let execute_fn = instance
.get_typed_func::<(u32, u32), u64>(&mut store, "vex_execute")
.map_err(|_| {
ToolError::execution_failed(
self.definition.name,
"WASM module must export 'vex_execute(u32, u32) -> u64'",
)
})?;
let memory = instance.get_memory(&mut store, "memory").ok_or_else(|| {
ToolError::execution_failed(self.definition.name, "WASM module must export 'memory'")
})?;
let input_json = serde_json::to_vec(&args)
.map_err(|e| ToolError::invalid_args(self.definition.name, e.to_string()))?;
let input_len = input_json.len() as u32;
let input_ptr = allocate
.call_async(&mut store, input_len)
.await
.map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to allocate WASM memory: {}", e),
)
})?;
memory
.write(&mut store, input_ptr as usize, &input_json)
.map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to write to WASM memory: {}", e),
)
})?;
let result_packed = execute_fn
.call_async(&mut store, (input_ptr, input_len))
.await
.map_err(|e| {
if format!("{:?}", e).contains("OutOfFuel") {
ToolError::timeout(self.definition.name, 0)
} else {
ToolError::execution_failed(
self.definition.name,
format!("WASM execution trapped: {}", e),
)
}
})?;
let output_ptr = (result_packed >> 32) as u32;
let output_len = (result_packed & 0xFFFFFFFF) as u32;
const MAX_WASM_OUTPUT_BYTES: u32 = 10 * 1024 * 1024; if output_len > MAX_WASM_OUTPUT_BYTES {
return Err(ToolError::execution_failed(
self.definition.name,
format!("WASM returned an output size exceeding the strict 10MB limit ({} bytes requested)", output_len),
));
}
let mut output_buf = vec![0u8; output_len as usize];
memory
.read(&mut store, output_ptr as usize, &mut output_buf)
.map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("Failed to read from WASM memory: {}", e),
)
})?;
let output_value: Value = serde_json::from_slice(&output_buf).map_err(|e| {
ToolError::execution_failed(
self.definition.name,
format!("WASM returned invalid JSON: {}", e),
)
})?;
Ok(output_value)
}
}