use async_trait::async_trait;
use tracing::{debug, warn};
use crate::{
BackendCapabilities, CodeExecutor, ExecutionError, ExecutionIsolation, ExecutionLanguage,
ExecutionPayload, ExecutionRequest, ExecutionResult, ExecutionStatus, GuestModuleFormat,
validate_request,
};
const WASM_MAGIC: &[u8] = b"\0asm";
const WASM_MIN_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct WasmGuestConfig {
pub max_memory_bytes: usize,
pub max_fuel: Option<u64>,
}
impl Default for WasmGuestConfig {
fn default() -> Self {
Self {
max_memory_bytes: 64 * 1024 * 1024, max_fuel: Some(1_000_000_000), }
}
}
#[derive(Debug, Clone)]
pub struct WasmGuestExecutor {
config: WasmGuestConfig,
}
impl WasmGuestExecutor {
pub fn new() -> Self {
Self { config: WasmGuestConfig::default() }
}
pub fn with_config(config: WasmGuestConfig) -> Self {
Self { config }
}
}
impl Default for WasmGuestExecutor {
fn default() -> Self {
Self::new()
}
}
pub fn validate_wasm_bytes(bytes: &[u8]) -> Result<(), ExecutionError> {
if bytes.len() < WASM_MIN_SIZE {
return Err(ExecutionError::InvalidRequest(format!(
"WASM module too small: {} bytes (minimum {WASM_MIN_SIZE})",
bytes.len()
)));
}
if !bytes.starts_with(WASM_MAGIC) {
return Err(ExecutionError::InvalidRequest(
"invalid WASM module: missing magic number (\\0asm)".to_string(),
));
}
Ok(())
}
#[async_trait]
impl CodeExecutor for WasmGuestExecutor {
fn name(&self) -> &str {
"wasm-guest"
}
fn capabilities(&self) -> BackendCapabilities {
BackendCapabilities {
isolation: ExecutionIsolation::InProcess,
enforce_network_policy: true,
enforce_filesystem_policy: true,
enforce_environment_policy: true,
enforce_timeout: true,
supports_structured_output: true,
supports_process_execution: false,
supports_persistent_workspace: false,
supports_interactive_sessions: false,
}
}
fn supports_language(&self, lang: &ExecutionLanguage) -> bool {
matches!(lang, ExecutionLanguage::Wasm)
}
async fn execute(&self, request: ExecutionRequest) -> Result<ExecutionResult, ExecutionError> {
validate_request(&self.capabilities(), &[ExecutionLanguage::Wasm], &request)?;
let bytes = match &request.payload {
ExecutionPayload::GuestModule { format, bytes } => {
match format {
GuestModuleFormat::Wasm => {}
}
bytes.clone()
}
ExecutionPayload::Source { .. } => {
return Err(ExecutionError::InvalidRequest(
"WasmGuestExecutor requires a GuestModule payload, not Source. \
For JavaScript source execution, use EmbeddedJsExecutor or ContainerCommandExecutor."
.to_string(),
));
}
};
validate_wasm_bytes(&bytes)?;
debug!(
module_size = bytes.len(),
max_memory = self.config.max_memory_bytes,
max_fuel = ?self.config.max_fuel,
"validating WASM guest module"
);
warn!("WASM guest execution is phase 1 placeholder — module validated but not executed");
Ok(ExecutionResult {
status: ExecutionStatus::Success,
stdout: String::new(),
stderr: "WASM guest execution: module validated (phase 1 placeholder — \
full runtime integration pending)"
.to_string(),
output: Some(serde_json::json!({
"phase": 1,
"module_size_bytes": bytes.len(),
"validated": true,
"executed": false,
"note": "Full WASM runtime integration is deferred to a later phase. \
The module passed structural validation."
})),
exit_code: Some(0),
stdout_truncated: false,
stderr_truncated: false,
duration_ms: 0,
metadata: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SandboxPolicy;
fn minimal_wasm_module() -> Vec<u8> {
vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]
}
#[test]
fn capabilities_are_in_process() {
let executor = WasmGuestExecutor::new();
let caps = executor.capabilities();
assert_eq!(caps.isolation, ExecutionIsolation::InProcess);
assert!(caps.enforce_network_policy);
assert!(caps.enforce_filesystem_policy);
assert!(caps.enforce_environment_policy);
assert!(caps.enforce_timeout);
assert!(caps.supports_structured_output);
assert!(!caps.supports_process_execution);
assert!(!caps.supports_persistent_workspace);
assert!(!caps.supports_interactive_sessions);
}
#[test]
fn supports_only_wasm() {
let executor = WasmGuestExecutor::new();
assert!(executor.supports_language(&ExecutionLanguage::Wasm));
assert!(!executor.supports_language(&ExecutionLanguage::JavaScript));
assert!(!executor.supports_language(&ExecutionLanguage::Rust));
assert!(!executor.supports_language(&ExecutionLanguage::Python));
assert!(!executor.supports_language(&ExecutionLanguage::Command));
}
#[test]
fn validate_wasm_bytes_valid() {
assert!(validate_wasm_bytes(&minimal_wasm_module()).is_ok());
}
#[test]
fn validate_wasm_bytes_too_short() {
let err = validate_wasm_bytes(b"\0asm").unwrap_err();
assert!(matches!(err, ExecutionError::InvalidRequest(_)));
assert!(err.to_string().contains("too small"));
}
#[test]
fn validate_wasm_bytes_wrong_magic() {
let err = validate_wasm_bytes(b"not_wasm_at_all!").unwrap_err();
assert!(matches!(err, ExecutionError::InvalidRequest(_)));
assert!(err.to_string().contains("magic number"));
}
#[test]
fn validate_wasm_bytes_empty() {
let err = validate_wasm_bytes(b"").unwrap_err();
assert!(matches!(err, ExecutionError::InvalidRequest(_)));
}
#[tokio::test]
async fn rejects_source_payload() {
let executor = WasmGuestExecutor::new();
let request = ExecutionRequest {
language: ExecutionLanguage::Wasm,
payload: ExecutionPayload::Source { code: "console.log('hello')".to_string() },
argv: vec![],
stdin: None,
input: None,
sandbox: SandboxPolicy::strict_rust(),
identity: None,
};
let err = executor.execute(request).await.unwrap_err();
assert!(matches!(err, ExecutionError::InvalidRequest(_)));
}
#[tokio::test]
async fn accepts_valid_wasm_module() {
let executor = WasmGuestExecutor::new();
let request = ExecutionRequest {
language: ExecutionLanguage::Wasm,
payload: ExecutionPayload::GuestModule {
format: GuestModuleFormat::Wasm,
bytes: minimal_wasm_module(),
},
argv: vec![],
stdin: None,
input: None,
sandbox: SandboxPolicy::strict_rust(),
identity: None,
};
let result = executor.execute(request).await.unwrap();
assert_eq!(result.status, ExecutionStatus::Success);
assert!(result.output.is_some());
let output = result.output.unwrap();
assert_eq!(output["validated"], true);
assert_eq!(output["executed"], false);
assert_eq!(output["module_size_bytes"], 8);
}
#[tokio::test]
async fn rejects_invalid_wasm_bytes() {
let executor = WasmGuestExecutor::new();
let request = ExecutionRequest {
language: ExecutionLanguage::Wasm,
payload: ExecutionPayload::GuestModule {
format: GuestModuleFormat::Wasm,
bytes: b"not_wasm".to_vec(),
},
argv: vec![],
stdin: None,
input: None,
sandbox: SandboxPolicy::strict_rust(),
identity: None,
};
let err = executor.execute(request).await.unwrap_err();
assert!(matches!(err, ExecutionError::InvalidRequest(_)));
}
#[tokio::test]
async fn rejects_javascript_language() {
let executor = WasmGuestExecutor::new();
let request = ExecutionRequest {
language: ExecutionLanguage::JavaScript,
payload: ExecutionPayload::GuestModule {
format: GuestModuleFormat::Wasm,
bytes: minimal_wasm_module(),
},
argv: vec![],
stdin: None,
input: None,
sandbox: SandboxPolicy::strict_rust(),
identity: None,
};
let err = executor.execute(request).await.unwrap_err();
assert!(
matches!(err, ExecutionError::UnsupportedLanguage(_))
|| matches!(err, ExecutionError::InvalidRequest(_))
);
}
#[test]
fn default_config_values() {
let config = WasmGuestConfig::default();
assert_eq!(config.max_memory_bytes, 64 * 1024 * 1024);
assert_eq!(config.max_fuel, Some(1_000_000_000));
}
}