309 lines
9.7 KiB
Rust
309 lines
9.7 KiB
Rust
// 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<Arc<WasmRuntime>>,
|
|
docker_pool: Option<Arc<DockerPool>>,
|
|
config: DispatcherConfig,
|
|
}
|
|
|
|
impl SandboxDispatcher {
|
|
/// Create a new dispatcher with both tiers
|
|
pub async fn new(
|
|
wasm_runtime: Option<Arc<WasmRuntime>>,
|
|
docker_pool: Option<Arc<DockerPool>>,
|
|
) -> crate::Result<Self> {
|
|
Ok(Self {
|
|
wasm_runtime,
|
|
docker_pool,
|
|
config: DispatcherConfig::default(),
|
|
})
|
|
}
|
|
|
|
/// Create with custom configuration
|
|
pub async fn with_config(
|
|
wasm_runtime: Option<Arc<WasmRuntime>>,
|
|
docker_pool: Option<Arc<DockerPool>>,
|
|
config: DispatcherConfig,
|
|
) -> crate::Result<Self> {
|
|
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<SandboxResult> {
|
|
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<SandboxResult> {
|
|
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);
|
|
}
|
|
}
|