Skip to main content

adk_code/
wasm_guest.rs

1//! WASM guest module executor — secondary backend for portable sandboxed plugins.
2//!
3//! [`WasmGuestExecutor`] executes precompiled `.wasm` guest modules inside a
4//! constrained runtime with a narrow host ABI. It is clearly separate from raw
5//! JavaScript source execution and from the flagship [`crate::RustSandboxExecutor`].
6//!
7//! # Product Posture
8//!
9//! This is a secondary backend for portable guest-module execution. The primary
10//! code-execution path remains authored Rust via [`crate::RustSandboxExecutor`].
11//!
12//! # Guest Module Contract
13//!
14//! Guest modules must:
15//!
16//! - Be valid WebAssembly binary format (`.wasm`)
17//! - Export a `run` function that accepts and returns i32 pointers to JSON buffers
18//! - Use only the narrow host ABI provided by the executor
19//!
20//! # Phase 1 Scope
21//!
22//! Phase 1 provides a placeholder implementation that validates guest module
23//! format and boundaries. Full WASM runtime integration (e.g., `wasmtime` or
24//! `wasmer`) is deferred to a later phase when the Rust-first path is stable.
25//!
26//! # Isolation Model
27//!
28//! | Capability | Enforced | Mechanism |
29//! |---|---|---|
30//! | Network policy | Yes | Guest modules have no network access by default |
31//! | Filesystem policy | Yes | Guest modules have no filesystem access by default |
32//! | Environment policy | Yes | Guest modules have no environment access |
33//! | Timeout | Yes | Fuel-based or wall-clock limits |
34//! | Structured output | Yes | JSON via host ABI |
35//!
36//! # Example
37//!
38//! ```rust
39//! use adk_code::{
40//!     CodeExecutor, WasmGuestExecutor, ExecutionIsolation, ExecutionLanguage,
41//! };
42//!
43//! let executor = WasmGuestExecutor::new();
44//! assert_eq!(executor.name(), "wasm-guest");
45//! assert_eq!(executor.capabilities().isolation, ExecutionIsolation::InProcess);
46//! assert!(executor.supports_language(&ExecutionLanguage::Wasm));
47//! assert!(!executor.supports_language(&ExecutionLanguage::JavaScript));
48//! ```
49
50use 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
59/// The WebAssembly binary magic number (`\0asm`).
60const WASM_MAGIC: &[u8] = b"\0asm";
61
62/// Minimum valid WASM module size (magic + version = 8 bytes).
63const WASM_MIN_SIZE: usize = 8;
64
65/// Configuration for the WASM guest executor.
66///
67/// # Example
68///
69/// ```rust
70/// use adk_code::WasmGuestConfig;
71///
72/// let config = WasmGuestConfig::default();
73/// assert_eq!(config.max_memory_bytes, 64 * 1024 * 1024);
74/// ```
75#[derive(Debug, Clone)]
76pub struct WasmGuestConfig {
77    /// Maximum memory in bytes the guest module may use.
78    pub max_memory_bytes: usize,
79    /// Maximum fuel (instruction count) for execution, if supported.
80    pub max_fuel: Option<u64>,
81}
82
83impl Default for WasmGuestConfig {
84    fn default() -> Self {
85        Self {
86            max_memory_bytes: 64 * 1024 * 1024, // 64 MB
87            max_fuel: Some(1_000_000_000),      // 1 billion instructions
88        }
89    }
90}
91
92/// Guest-module backend for precompiled `.wasm` modules.
93///
94/// Executes guest modules through a constrained runtime with a narrow host ABI.
95/// This is clearly separate from raw JavaScript source execution — it accepts
96/// only [`ExecutionPayload::GuestModule`] payloads with [`GuestModuleFormat::Wasm`].
97///
98/// # Important Distinction
99///
100/// `WasmGuestExecutor` is NOT a JavaScript executor. It runs precompiled
101/// WebAssembly binary modules. For JavaScript source execution, use
102/// [`crate::EmbeddedJsExecutor`] (secondary scripting) or
103/// [`crate::ContainerCommandExecutor`] (container-isolated Node.js).
104///
105/// # Example
106///
107/// ```rust
108/// use adk_code::{CodeExecutor, WasmGuestExecutor, ExecutionLanguage};
109///
110/// let executor = WasmGuestExecutor::new();
111/// assert!(executor.supports_language(&ExecutionLanguage::Wasm));
112/// assert!(!executor.supports_language(&ExecutionLanguage::JavaScript));
113/// assert!(!executor.supports_language(&ExecutionLanguage::Rust));
114/// ```
115#[derive(Debug, Clone)]
116pub struct WasmGuestExecutor {
117    config: WasmGuestConfig,
118}
119
120impl WasmGuestExecutor {
121    /// Create a new WASM guest executor with default configuration.
122    pub fn new() -> Self {
123        Self { config: WasmGuestConfig::default() }
124    }
125
126    /// Create a new WASM guest executor with the given configuration.
127    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
138/// Validate that the bytes represent a valid WASM binary module.
139///
140/// Checks the magic number (`\0asm`) and minimum size. This is a structural
141/// validation — it does not verify the module's internal consistency or
142/// exported functions.
143///
144/// # Example
145///
146/// ```rust
147/// use adk_code::validate_wasm_bytes;
148///
149/// // Valid WASM magic + version 1
150/// let valid = b"\0asm\x01\x00\x00\x00";
151/// assert!(validate_wasm_bytes(valid).is_ok());
152///
153/// // Too short
154/// assert!(validate_wasm_bytes(b"\0asm").is_err());
155///
156/// // Wrong magic
157/// assert!(validate_wasm_bytes(b"not_wasm_at_all!").is_err());
158/// ```
159pub 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            // WASM guest modules have no access to host resources by default.
186            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        // Extract and validate the guest module bytes.
205        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        // Phase 1: Structural validation only.
231        // Full WASM runtime integration is deferred until the Rust-first path
232        // is stable. This placeholder validates the module format and returns
233        // a descriptive result indicating the module was accepted but not executed.
234        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    /// A minimal valid WASM module (magic + version 1 + empty sections).
265    fn minimal_wasm_module() -> Vec<u8> {
266        // \0asm followed by version 1 (little-endian u32)
267        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        // validate_request catches this before execute body
334        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        // validate_request catches language mismatch
401        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}