use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use wasmtime::{Config, Engine, OptLevel};
use crate::tools::wasm::error::WasmError;
use crate::tools::wasm::limits::{FuelConfig, ResourceLimits};
pub const EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(500);
pub fn enable_compilation_cache(
wasmtime_config: &mut Config,
label: &str,
explicit_dir: Option<&Path>,
) -> anyhow::Result<()> {
let custom_dir = match explicit_dir {
Some(dir) => Some(dir.to_path_buf()),
#[cfg(windows)]
None => {
let base = dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join("ironclaw");
Some(base.join(format!("wasmtime-{}", label)))
}
#[cfg(not(windows))]
None => {
let _ = label;
None
}
};
match custom_dir {
Some(dir) => {
std::fs::create_dir_all(&dir)?;
let toml_path = dir.join("wasmtime-cache.toml");
let escaped = dir
.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\"");
let toml_content = format!("[cache]\nenabled = true\ndirectory = \"{}\"\n", escaped);
std::fs::write(&toml_path, toml_content)?;
wasmtime_config.cache_config_load(&toml_path)?;
Ok(())
}
None => {
wasmtime_config.cache_config_load_default()?;
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub struct WasmRuntimeConfig {
pub default_limits: ResourceLimits,
pub fuel_config: FuelConfig,
pub cache_compiled: bool,
pub cache_dir: Option<PathBuf>,
pub optimization_level: OptLevel,
}
impl Default for WasmRuntimeConfig {
fn default() -> Self {
Self {
default_limits: ResourceLimits::default(),
fuel_config: FuelConfig::default(),
cache_compiled: true,
cache_dir: None,
optimization_level: OptLevel::Speed,
}
}
}
impl WasmRuntimeConfig {
pub fn for_testing() -> Self {
Self {
default_limits: ResourceLimits::default()
.with_memory(1024 * 1024) .with_fuel(100_000)
.with_timeout(Duration::from_secs(5)),
fuel_config: FuelConfig::with_limit(100_000),
cache_compiled: false,
cache_dir: None,
optimization_level: OptLevel::None, }
}
}
pub struct PreparedModule {
pub name: String,
pub description: String,
pub schema: serde_json::Value,
component: wasmtime::component::Component,
pub limits: ResourceLimits,
}
impl PreparedModule {
pub fn component(&self) -> &wasmtime::component::Component {
&self.component
}
}
impl std::fmt::Debug for PreparedModule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreparedModule")
.field("name", &self.name)
.field("description", &self.description)
.field("limits", &self.limits)
.finish()
}
}
pub struct WasmToolRuntime {
engine: Engine,
config: WasmRuntimeConfig,
modules: RwLock<HashMap<String, Arc<PreparedModule>>>,
}
impl WasmToolRuntime {
pub fn new(config: WasmRuntimeConfig) -> Result<Self, WasmError> {
let mut wasmtime_config = Config::new();
if config.fuel_config.enabled {
wasmtime_config.consume_fuel(true);
}
wasmtime_config.epoch_interruption(true);
wasmtime_config.wasm_component_model(true);
wasmtime_config.wasm_threads(false);
wasmtime_config.cranelift_opt_level(config.optimization_level);
wasmtime_config.debug_info(false);
if let Err(e) =
enable_compilation_cache(&mut wasmtime_config, "tools", config.cache_dir.as_deref())
{
tracing::warn!("Failed to enable wasmtime compilation cache: {}", e);
}
let engine = Engine::new(&wasmtime_config).map_err(|e| {
WasmError::EngineCreationFailed(format!("Failed to create Wasmtime engine: {}", e))
})?;
let ticker_engine = engine.clone();
std::thread::Builder::new()
.name("wasm-epoch-ticker".into())
.spawn(move || {
loop {
std::thread::sleep(EPOCH_TICK_INTERVAL);
ticker_engine.increment_epoch();
}
})
.map_err(|e| {
WasmError::EngineCreationFailed(format!(
"Failed to spawn epoch ticker thread: {}",
e
))
})?;
Ok(Self {
engine,
config,
modules: RwLock::new(HashMap::new()),
})
}
pub fn engine(&self) -> &Engine {
&self.engine
}
pub fn config(&self) -> &WasmRuntimeConfig {
&self.config
}
pub async fn prepare(
&self,
name: &str,
wasm_bytes: &[u8],
limits: Option<ResourceLimits>,
) -> Result<Arc<PreparedModule>, WasmError> {
if let Some(module) = self.modules.read().await.get(name) {
return Ok(Arc::clone(module));
}
let name = name.to_string();
let wasm_bytes = wasm_bytes.to_vec();
let engine = self.engine.clone();
let default_limits = self.config.default_limits.clone();
let prepared = tokio::task::spawn_blocking(move || {
let component = wasmtime::component::Component::new(&engine, &wasm_bytes)
.map_err(|e| WasmError::CompilationFailed(e.to_string()))?;
let effective_limits = limits.clone().unwrap_or(default_limits.clone());
let (description, schema) = crate::tools::wasm::wrapper::extract_wasm_metadata(
&engine,
&component,
&effective_limits,
)
.unwrap_or_else(|e| {
tracing::warn!(
name = %name,
error = %e,
"WASM metadata extraction failed, using fallbacks"
);
(
"WASM sandboxed tool".to_string(),
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": true
}),
)
});
Ok::<_, WasmError>(PreparedModule {
name: name.clone(),
description,
schema,
component,
limits: limits.unwrap_or(default_limits),
})
})
.await
.map_err(|e| WasmError::ExecutionPanicked(format!("Preparation task panicked: {}", e)))??;
let prepared = Arc::new(prepared);
if self.config.cache_compiled {
self.modules
.write()
.await
.insert(prepared.name.clone(), Arc::clone(&prepared));
}
tracing::debug!(
name = %prepared.name,
"Prepared WASM tool for execution"
);
Ok(prepared)
}
pub async fn get(&self, name: &str) -> Option<Arc<PreparedModule>> {
self.modules.read().await.get(name).cloned()
}
pub async fn remove(&self, name: &str) -> Option<Arc<PreparedModule>> {
self.modules.write().await.remove(name)
}
pub async fn list(&self) -> Vec<String> {
self.modules.read().await.keys().cloned().collect()
}
pub async fn clear(&self) {
self.modules.write().await.clear();
}
}
impl std::fmt::Debug for WasmToolRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WasmToolRuntime")
.field("config", &self.config)
.field("modules", &"<RwLock<HashMap>>")
.finish()
}
}
#[cfg(test)]
mod tests {
use crate::tools::wasm::limits::ResourceLimits;
use crate::tools::wasm::runtime::{WasmRuntimeConfig, WasmToolRuntime};
#[test]
fn test_runtime_config_default() {
let config = WasmRuntimeConfig::default();
assert!(config.cache_compiled);
assert!(config.fuel_config.enabled);
}
#[test]
fn test_runtime_config_for_testing() {
let config = WasmRuntimeConfig::for_testing();
assert!(!config.cache_compiled);
assert_eq!(config.default_limits.memory_bytes, 1024 * 1024);
}
#[test]
fn test_runtime_creation() {
let config = WasmRuntimeConfig::for_testing();
let runtime = WasmToolRuntime::new(config).unwrap();
assert!(runtime.config().fuel_config.enabled);
}
#[tokio::test]
async fn test_module_cache_operations() {
let config = WasmRuntimeConfig::for_testing();
let runtime = WasmToolRuntime::new(config).unwrap();
assert!(runtime.list().await.is_empty());
assert!(runtime.get("test").await.is_none());
}
#[test]
fn test_prepared_module_limits() {
let limits = ResourceLimits::default()
.with_memory(5 * 1024 * 1024)
.with_fuel(500_000);
assert_eq!(limits.memory_bytes, 5 * 1024 * 1024);
assert_eq!(limits.fuel, 500_000);
}
#[test]
fn test_enable_compilation_cache_with_explicit_dir() {
use crate::tools::wasm::runtime::enable_compilation_cache;
let tmp = tempfile::tempdir().expect("failed to create temp dir");
let cache_dir = tmp.path().join("custom-cache");
let mut config = wasmtime::Config::new();
enable_compilation_cache(&mut config, "test-engine", Some(cache_dir.as_path()))
.expect("enable_compilation_cache should succeed with explicit dir");
assert!(cache_dir.exists(), "cache directory should be created");
let toml_path = cache_dir.join("wasmtime-cache.toml");
assert!(toml_path.exists(), "TOML config should be written");
let content = std::fs::read_to_string(&toml_path).unwrap();
assert!(
content.contains("[cache]"),
"TOML must contain [cache] section"
);
assert!(content.contains("enabled = true"), "cache must be enabled");
}
#[test]
fn test_enable_compilation_cache_label_isolation() {
use crate::tools::wasm::runtime::enable_compilation_cache;
let tmp = tempfile::tempdir().expect("failed to create temp dir");
let base = tmp.path().join("isolation");
let dir_a = base.join("engine-a");
let dir_b = base.join("engine-b");
let mut config_a = wasmtime::Config::new();
enable_compilation_cache(&mut config_a, "a", Some(dir_a.as_path()))
.expect("cache A should succeed");
let mut config_b = wasmtime::Config::new();
enable_compilation_cache(&mut config_b, "b", Some(dir_b.as_path()))
.expect("cache B should succeed");
assert!(dir_a.exists());
assert!(dir_b.exists());
assert_ne!(dir_a, dir_b);
}
#[test]
fn test_runtime_creation_without_tools_dir() {
let config = WasmRuntimeConfig::for_testing();
let runtime = WasmToolRuntime::new(config).expect("runtime should init without tools dir");
assert!(runtime.config().fuel_config.enabled);
}
}