use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::PathBuf;
pub use wasmtime;
pub use wasmtime_wasi;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Capability {
ReadFiles,
WriteFiles,
Network,
Environment,
Stdio,
Random,
Clock,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig {
pub max_memory_bytes: u64,
pub max_execution_secs: u64,
pub max_fuel: u64,
pub capabilities: HashSet<Capability>,
pub allowed_directories: Vec<PathBuf>,
pub allowed_hosts: Vec<String>,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
max_memory_bytes: 256 * 1024 * 1024, max_execution_secs: 30,
max_fuel: 1_000_000_000, capabilities: HashSet::new(), allowed_directories: Vec::new(),
allowed_hosts: Vec::new(),
}
}
}
impl SandboxConfig {
pub fn minimal() -> Self {
Self::default()
}
pub fn read_only(directories: Vec<PathBuf>) -> Self {
let mut config = Self::default();
config.capabilities.insert(Capability::ReadFiles);
config.capabilities.insert(Capability::Stdio);
config.allowed_directories = directories;
config
}
pub fn with_network(hosts: Vec<String>) -> Self {
let mut config = Self::default();
config.capabilities.insert(Capability::Network);
config.capabilities.insert(Capability::Stdio);
config.allowed_hosts = hosts;
config
}
pub fn with_capability(mut self, cap: Capability) -> Self {
self.capabilities.insert(cap);
self
}
pub fn has_capability(&self, cap: Capability) -> bool {
self.capabilities.contains(&cap)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub fuel_consumed: u64,
pub execution_time_ms: u64,
}
pub struct WasmSandbox {
config: SandboxConfig,
}
impl WasmSandbox {
pub fn new(config: SandboxConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &SandboxConfig {
&self.config
}
pub async fn execute(
&self,
wasm_bytes: &[u8],
args: &[String],
env: &[(String, String)],
) -> Result<ExecutionResult> {
use wasmtime::*;
use wasmtime_wasi::WasiCtxBuilder;
let start = std::time::Instant::now();
let mut engine_config = Config::new();
engine_config.consume_fuel(true);
let engine = Engine::new(&engine_config)?;
let mut store = Store::new(&engine, ());
store.set_fuel(self.config.max_fuel)?;
let mut wasi_builder = WasiCtxBuilder::new();
if self.config.has_capability(Capability::Stdio) {
wasi_builder.inherit_stdio();
}
if self.config.has_capability(Capability::Environment) {
for (key, value) in env {
wasi_builder.env(key, value);
}
}
wasi_builder.args(args);
let _wasi = wasi_builder.build_p1();
let module = Module::from_binary(&engine, wasm_bytes)?;
let linker = Linker::new(&engine);
let _instance = linker.instantiate(&mut store, &module)?;
let fuel_consumed = self.config.max_fuel - store.get_fuel()?;
let execution_time_ms = start.elapsed().as_millis() as u64;
Ok(ExecutionResult {
exit_code: 0,
stdout: String::new(),
stderr: String::new(),
fuel_consumed,
execution_time_ms,
})
}
pub fn validate(&self, wasm_bytes: &[u8]) -> Result<ModuleInfo> {
use wasmtime::*;
let engine = Engine::default();
let module = Module::from_binary(&engine, wasm_bytes)?;
Ok(ModuleInfo {
name: module.name().map(|s| s.to_string()),
imports: module.imports().map(|i| i.name().to_string()).collect(),
exports: module.exports().map(|e| e.name().to_string()).collect(),
})
}
}
impl Default for WasmSandbox {
fn default() -> Self {
Self::new(SandboxConfig::default())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleInfo {
pub name: Option<String>,
pub imports: Vec<String>,
pub exports: Vec<String>,
}
pub struct PluginManager {
sandbox: WasmSandbox,
loaded_plugins: Vec<LoadedPlugin>,
}
#[derive(Debug, Clone)]
pub struct LoadedPlugin {
pub name: String,
pub info: ModuleInfo,
pub wasm_bytes: Vec<u8>,
}
impl PluginManager {
pub fn new(config: SandboxConfig) -> Self {
Self {
sandbox: WasmSandbox::new(config),
loaded_plugins: Vec::new(),
}
}
pub fn load_plugin(&mut self, name: &str, wasm_bytes: Vec<u8>) -> Result<&LoadedPlugin> {
let info = self.sandbox.validate(&wasm_bytes)?;
self.loaded_plugins.push(LoadedPlugin {
name: name.to_string(),
info,
wasm_bytes,
});
Ok(self.loaded_plugins.last().unwrap())
}
pub fn get_plugin(&self, name: &str) -> Option<&LoadedPlugin> {
self.loaded_plugins.iter().find(|p| p.name == name)
}
pub async fn execute_plugin(&self, name: &str, args: &[String]) -> Result<ExecutionResult> {
let plugin = self
.get_plugin(name)
.ok_or_else(|| anyhow::anyhow!("Plugin not found: {}", name))?;
self.sandbox.execute(&plugin.wasm_bytes, args, &[]).await
}
pub fn list_plugins(&self) -> Vec<&str> {
self.loaded_plugins
.iter()
.map(|p| p.name.as_str())
.collect()
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new(SandboxConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_config_default() {
let config = SandboxConfig::default();
assert!(config.capabilities.is_empty());
assert_eq!(config.max_execution_secs, 30);
}
#[test]
fn test_sandbox_config_minimal() {
let config = SandboxConfig::minimal();
assert!(!config.has_capability(Capability::Network));
assert!(!config.has_capability(Capability::ReadFiles));
}
#[test]
fn test_sandbox_config_with_capability() {
let config = SandboxConfig::default()
.with_capability(Capability::Stdio)
.with_capability(Capability::Clock);
assert!(config.has_capability(Capability::Stdio));
assert!(config.has_capability(Capability::Clock));
assert!(!config.has_capability(Capability::Network));
}
#[test]
fn test_plugin_manager() {
let manager = PluginManager::default();
assert!(manager.list_plugins().is_empty());
}
}