1use async_trait::async_trait;
51use tracing::{debug, warn};
52
53use crate::{
54 BackendCapabilities, CodeExecutor, ExecutionError, ExecutionIsolation, ExecutionLanguage,
55 ExecutionPayload, ExecutionRequest, ExecutionResult, ExecutionStatus, GuestModuleFormat,
56 validate_request,
57};
58
59const WASM_MAGIC: &[u8] = b"\0asm";
61
62const WASM_MIN_SIZE: usize = 8;
64
65#[derive(Debug, Clone)]
76pub struct WasmGuestConfig {
77 pub max_memory_bytes: usize,
79 pub max_fuel: Option<u64>,
81}
82
83impl Default for WasmGuestConfig {
84 fn default() -> Self {
85 Self {
86 max_memory_bytes: 64 * 1024 * 1024, max_fuel: Some(1_000_000_000), }
89 }
90}
91
92#[derive(Debug, Clone)]
116pub struct WasmGuestExecutor {
117 config: WasmGuestConfig,
118}
119
120impl WasmGuestExecutor {
121 pub fn new() -> Self {
123 Self { config: WasmGuestConfig::default() }
124 }
125
126 pub fn with_config(config: WasmGuestConfig) -> Self {
128 Self { config }
129 }
130}
131
132impl Default for WasmGuestExecutor {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138pub fn validate_wasm_bytes(bytes: &[u8]) -> Result<(), ExecutionError> {
160 if bytes.len() < WASM_MIN_SIZE {
161 return Err(ExecutionError::InvalidRequest(format!(
162 "WASM module too small: {} bytes (minimum {WASM_MIN_SIZE})",
163 bytes.len()
164 )));
165 }
166
167 if !bytes.starts_with(WASM_MAGIC) {
168 return Err(ExecutionError::InvalidRequest(
169 "invalid WASM module: missing magic number (\\0asm)".to_string(),
170 ));
171 }
172
173 Ok(())
174}
175
176#[async_trait]
177impl CodeExecutor for WasmGuestExecutor {
178 fn name(&self) -> &str {
179 "wasm-guest"
180 }
181
182 fn capabilities(&self) -> BackendCapabilities {
183 BackendCapabilities {
184 isolation: ExecutionIsolation::InProcess,
185 enforce_network_policy: true,
187 enforce_filesystem_policy: true,
188 enforce_environment_policy: true,
189 enforce_timeout: true,
190 supports_structured_output: true,
191 supports_process_execution: false,
192 supports_persistent_workspace: false,
193 supports_interactive_sessions: false,
194 }
195 }
196
197 fn supports_language(&self, lang: &ExecutionLanguage) -> bool {
198 matches!(lang, ExecutionLanguage::Wasm)
199 }
200
201 async fn execute(&self, request: ExecutionRequest) -> Result<ExecutionResult, ExecutionError> {
202 validate_request(&self.capabilities(), &[ExecutionLanguage::Wasm], &request)?;
203
204 let bytes = match &request.payload {
206 ExecutionPayload::GuestModule { format, bytes } => {
207 match format {
208 GuestModuleFormat::Wasm => {}
209 }
210 bytes.clone()
211 }
212 ExecutionPayload::Source { .. } => {
213 return Err(ExecutionError::InvalidRequest(
214 "WasmGuestExecutor requires a GuestModule payload, not Source. \
215 For JavaScript source execution, use EmbeddedJsExecutor or ContainerCommandExecutor."
216 .to_string(),
217 ));
218 }
219 };
220
221 validate_wasm_bytes(&bytes)?;
222
223 debug!(
224 module_size = bytes.len(),
225 max_memory = self.config.max_memory_bytes,
226 max_fuel = ?self.config.max_fuel,
227 "validating WASM guest module"
228 );
229
230 warn!("WASM guest execution is phase 1 placeholder — module validated but not executed");
235
236 Ok(ExecutionResult {
237 status: ExecutionStatus::Success,
238 stdout: String::new(),
239 stderr: "WASM guest execution: module validated (phase 1 placeholder — \
240 full runtime integration pending)"
241 .to_string(),
242 output: Some(serde_json::json!({
243 "phase": 1,
244 "module_size_bytes": bytes.len(),
245 "validated": true,
246 "executed": false,
247 "note": "Full WASM runtime integration is deferred to a later phase. \
248 The module passed structural validation."
249 })),
250 exit_code: Some(0),
251 stdout_truncated: false,
252 stderr_truncated: false,
253 duration_ms: 0,
254 metadata: None,
255 })
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::SandboxPolicy;
263
264 fn minimal_wasm_module() -> Vec<u8> {
266 vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]
268 }
269
270 #[test]
271 fn capabilities_are_in_process() {
272 let executor = WasmGuestExecutor::new();
273 let caps = executor.capabilities();
274 assert_eq!(caps.isolation, ExecutionIsolation::InProcess);
275 assert!(caps.enforce_network_policy);
276 assert!(caps.enforce_filesystem_policy);
277 assert!(caps.enforce_environment_policy);
278 assert!(caps.enforce_timeout);
279 assert!(caps.supports_structured_output);
280 assert!(!caps.supports_process_execution);
281 assert!(!caps.supports_persistent_workspace);
282 assert!(!caps.supports_interactive_sessions);
283 }
284
285 #[test]
286 fn supports_only_wasm() {
287 let executor = WasmGuestExecutor::new();
288 assert!(executor.supports_language(&ExecutionLanguage::Wasm));
289 assert!(!executor.supports_language(&ExecutionLanguage::JavaScript));
290 assert!(!executor.supports_language(&ExecutionLanguage::Rust));
291 assert!(!executor.supports_language(&ExecutionLanguage::Python));
292 assert!(!executor.supports_language(&ExecutionLanguage::Command));
293 }
294
295 #[test]
296 fn validate_wasm_bytes_valid() {
297 assert!(validate_wasm_bytes(&minimal_wasm_module()).is_ok());
298 }
299
300 #[test]
301 fn validate_wasm_bytes_too_short() {
302 let err = validate_wasm_bytes(b"\0asm").unwrap_err();
303 assert!(matches!(err, ExecutionError::InvalidRequest(_)));
304 assert!(err.to_string().contains("too small"));
305 }
306
307 #[test]
308 fn validate_wasm_bytes_wrong_magic() {
309 let err = validate_wasm_bytes(b"not_wasm_at_all!").unwrap_err();
310 assert!(matches!(err, ExecutionError::InvalidRequest(_)));
311 assert!(err.to_string().contains("magic number"));
312 }
313
314 #[test]
315 fn validate_wasm_bytes_empty() {
316 let err = validate_wasm_bytes(b"").unwrap_err();
317 assert!(matches!(err, ExecutionError::InvalidRequest(_)));
318 }
319
320 #[tokio::test]
321 async fn rejects_source_payload() {
322 let executor = WasmGuestExecutor::new();
323 let request = ExecutionRequest {
324 language: ExecutionLanguage::Wasm,
325 payload: ExecutionPayload::Source { code: "console.log('hello')".to_string() },
326 argv: vec![],
327 stdin: None,
328 input: None,
329 sandbox: SandboxPolicy::strict_rust(),
330 identity: None,
331 };
332
333 let err = executor.execute(request).await.unwrap_err();
335 assert!(matches!(err, ExecutionError::InvalidRequest(_)));
336 }
337
338 #[tokio::test]
339 async fn accepts_valid_wasm_module() {
340 let executor = WasmGuestExecutor::new();
341 let request = ExecutionRequest {
342 language: ExecutionLanguage::Wasm,
343 payload: ExecutionPayload::GuestModule {
344 format: GuestModuleFormat::Wasm,
345 bytes: minimal_wasm_module(),
346 },
347 argv: vec![],
348 stdin: None,
349 input: None,
350 sandbox: SandboxPolicy::strict_rust(),
351 identity: None,
352 };
353
354 let result = executor.execute(request).await.unwrap();
355 assert_eq!(result.status, ExecutionStatus::Success);
356 assert!(result.output.is_some());
357 let output = result.output.unwrap();
358 assert_eq!(output["validated"], true);
359 assert_eq!(output["executed"], false);
360 assert_eq!(output["module_size_bytes"], 8);
361 }
362
363 #[tokio::test]
364 async fn rejects_invalid_wasm_bytes() {
365 let executor = WasmGuestExecutor::new();
366 let request = ExecutionRequest {
367 language: ExecutionLanguage::Wasm,
368 payload: ExecutionPayload::GuestModule {
369 format: GuestModuleFormat::Wasm,
370 bytes: b"not_wasm".to_vec(),
371 },
372 argv: vec![],
373 stdin: None,
374 input: None,
375 sandbox: SandboxPolicy::strict_rust(),
376 identity: None,
377 };
378
379 let err = executor.execute(request).await.unwrap_err();
380 assert!(matches!(err, ExecutionError::InvalidRequest(_)));
381 }
382
383 #[tokio::test]
384 async fn rejects_javascript_language() {
385 let executor = WasmGuestExecutor::new();
386 let request = ExecutionRequest {
387 language: ExecutionLanguage::JavaScript,
388 payload: ExecutionPayload::GuestModule {
389 format: GuestModuleFormat::Wasm,
390 bytes: minimal_wasm_module(),
391 },
392 argv: vec![],
393 stdin: None,
394 input: None,
395 sandbox: SandboxPolicy::strict_rust(),
396 identity: None,
397 };
398
399 let err = executor.execute(request).await.unwrap_err();
400 assert!(
402 matches!(err, ExecutionError::UnsupportedLanguage(_))
403 || matches!(err, ExecutionError::InvalidRequest(_))
404 );
405 }
406
407 #[test]
408 fn default_config_values() {
409 let config = WasmGuestConfig::default();
410 assert_eq!(config.max_memory_bytes, 64 * 1024 * 1024);
411 assert_eq!(config.max_fuel, Some(1_000_000_000));
412 }
413}