// Sandbox Dispatcher - Auto-tier selection // Routes commands to WASM (fast) or Docker (compatible) based on complexity use std::sync::Arc; use std::time::Instant; use tracing::{debug, info}; use crate::sandbox::docker_pool::DockerPool; use crate::sandbox::wasm_runtime::WasmRuntime; use crate::sandbox::{SandboxCommand, SandboxResult}; use crate::RLMError; /// Sandbox tier selection #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SandboxTier { /// Tier 1: WASM runtime (fast, <5ms target) Wasm, /// Tier 2: Docker pool (compatible, 80-150ms target) Docker, } impl std::fmt::Display for SandboxTier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SandboxTier::Wasm => write!(f, "wasm"), SandboxTier::Docker => write!(f, "docker"), } } } /// Dispatcher configuration #[derive(Debug, Clone)] pub struct DispatcherConfig { /// Enable WASM tier pub enable_wasm: bool, /// Enable Docker tier pub enable_docker: bool, /// Fallback to Docker if WASM fails pub fallback_to_docker: bool, } impl Default for DispatcherConfig { fn default() -> Self { Self { enable_wasm: true, enable_docker: true, fallback_to_docker: true, } } } /// Sandbox dispatcher - routes commands to appropriate tier pub struct SandboxDispatcher { wasm_runtime: Option>, docker_pool: Option>, config: DispatcherConfig, } impl SandboxDispatcher { /// Create a new dispatcher with both tiers pub async fn new( wasm_runtime: Option>, docker_pool: Option>, ) -> crate::Result { Ok(Self { wasm_runtime, docker_pool, config: DispatcherConfig::default(), }) } /// Create with custom configuration pub async fn with_config( wasm_runtime: Option>, docker_pool: Option>, config: DispatcherConfig, ) -> crate::Result { Ok(Self { wasm_runtime, docker_pool, config, }) } /// Execute a command, automatically selecting the appropriate tier /// /// # Tier Selection Logic /// 1. WASI-compatible commands → Tier 1 (WASM) if enabled /// 2. Complex commands → Tier 2 (Docker) if enabled /// 3. Fallback: Docker if WASM fails and fallback enabled /// /// # Returns /// SandboxResult with tier information pub async fn execute(&self, command: &SandboxCommand) -> crate::Result { let start = Instant::now(); // Select tier let tier = self.select_tier(&command.command); debug!( "Dispatching command '{}' to {:?} tier", command.command, tier ); // Execute in selected tier let result = match tier { SandboxTier::Wasm => { if let Some(ref wasm_runtime) = self.wasm_runtime { match wasm_runtime.execute(command) { Ok(result) => Ok(result), Err(e) if self.config.fallback_to_docker => { info!("WASM execution failed, falling back to Docker: {}", e); self.execute_docker(command).await } Err(e) => Err(e), } } else { return Err(RLMError::SandboxError( "WASM tier not available".to_string(), )); } } SandboxTier::Docker => self.execute_docker(command).await, }?; let duration = start.elapsed(); debug!( "Dispatched command '{}' via {} in {:?}", command.command, result.tier, duration ); Ok(result) } /// Execute in Docker tier async fn execute_docker(&self, command: &SandboxCommand) -> crate::Result { if let Some(ref docker_pool) = self.docker_pool { docker_pool.execute(command).await } else { Err(RLMError::SandboxError( "Docker tier not available".to_string(), )) } } /// Select tier based on command complexity fn select_tier(&self, command: &str) -> SandboxTier { // WASI-compatible commands go to WASM tier (if enabled and available) if self.config.enable_wasm && self.wasm_runtime.is_some() && self.is_wasi_compatible(command) { return SandboxTier::Wasm; } // Non-WASI commands prefer Docker (if enabled AND available) if self.config.enable_docker && self.docker_pool.is_some() { return SandboxTier::Docker; } // Fallback: If Docker enabled but unavailable, use WASM if available if self.config.enable_docker && self.docker_pool.is_none() && self.wasm_runtime.is_some() { return SandboxTier::Wasm; } // If WASM enabled and available (for non-WASI commands when Docker not // preferred) if self.config.enable_wasm && self.wasm_runtime.is_some() { return SandboxTier::Wasm; } // Last resort: Docker (will error on execute if not available) SandboxTier::Docker } /// Check if command is WASI-compatible fn is_wasi_compatible(&self, command: &str) -> bool { matches!(command, "peek" | "grep" | "slice") } /// Get tier usage statistics pub fn tier_stats(&self) -> TierStats { // In a real implementation, would track tier usage in metrics // For Phase 4, return basic info TierStats { wasm_available: self.wasm_runtime.is_some(), docker_available: self.docker_pool.is_some(), docker_pool_size: self .docker_pool .as_ref() .map(|p| p.pool_size()) .unwrap_or(0), } } } /// Tier usage statistics #[derive(Debug, Clone)] pub struct TierStats { pub wasm_available: bool, pub docker_available: bool, pub docker_pool_size: usize, } #[cfg(test)] mod tests { use super::*; use crate::sandbox::wasm_runtime::WasmRuntime; #[tokio::test] async fn test_dispatcher_creation() { let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap(); let stats = dispatcher.tier_stats(); assert!(stats.wasm_available); assert!(!stats.docker_available); } #[tokio::test] async fn test_tier_selection_wasi_compatible() { let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap(); assert_eq!(dispatcher.select_tier("peek"), SandboxTier::Wasm); assert_eq!(dispatcher.select_tier("grep"), SandboxTier::Wasm); assert_eq!(dispatcher.select_tier("slice"), SandboxTier::Wasm); } #[tokio::test] async fn test_tier_selection_complex_command() { let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap(); // Complex commands should prefer Docker (but WASM is selected as fallback if // Docker unavailable) assert_eq!(dispatcher.select_tier("bash"), SandboxTier::Wasm); // Fallback } #[tokio::test] async fn test_execute_wasm_tier() { let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap(); let command = SandboxCommand::new("peek") .arg("3") .stdin("line1\nline2\nline3\nline4"); let result = dispatcher.execute(&command).await.unwrap(); assert!(result.is_success()); assert_eq!(result.tier, SandboxTier::Wasm); assert_eq!(result.output, "line1\nline2\nline3\n"); } #[tokio::test] async fn test_execute_grep_wasm() { let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap(); let command = SandboxCommand::new("grep") .arg("error") .stdin("info: ok\nerror: failed\nwarn: retry"); let result = dispatcher.execute(&command).await.unwrap(); assert!(result.is_success()); assert_eq!(result.tier, SandboxTier::Wasm); assert!(result.output.contains("error: failed")); } #[tokio::test] async fn test_wasm_not_available() { let dispatcher = SandboxDispatcher::new(None, None).await.unwrap(); let command = SandboxCommand::new("peek").arg("5").stdin("test"); let result = dispatcher.execute(&command).await; assert!(result.is_err()); } #[tokio::test] async fn test_custom_config() { let config = DispatcherConfig { enable_wasm: false, enable_docker: false, fallback_to_docker: false, }; let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::with_config(wasm, None, config) .await .unwrap(); // With WASM disabled, should select Docker (even though unavailable) assert_eq!(dispatcher.select_tier("peek"), SandboxTier::Docker); } #[tokio::test] async fn test_tier_stats() { let wasm = Some(Arc::new(WasmRuntime::new())); let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap(); let stats = dispatcher.tier_stats(); assert!(stats.wasm_available); assert!(!stats.docker_available); assert_eq!(stats.docker_pool_size, 0); } }