Skip to main content

sochdb_kernel/
wasm_sandbox_runtime.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! WASM-Sandbox Crate Integration (Task 10)
19//!
20//! This module provides integration with the `wasm-sandbox` crate for running
21//! untrusted WASM plugins in a secure sandbox with:
22//!
23//! - Memory isolation
24//! - CPU time limits (fuel)
25//! - Syscall filtering
26//! - Capability-based access control
27//!
28//! ## Architecture
29//!
30//! ```text
31//! ┌─────────────────────────────────────────────────────────────────┐
32//! │                    SochDB Kernel                                │
33//! │  ┌───────────────────────────────────────────────────────────┐ │
34//! │  │              WasmSandboxRuntime                           │ │
35//! │  │  ┌─────────────────────────────────────────────────────┐ │ │
36//! │  │  │            Sandbox Manager                          │ │ │
37//! │  │  │  ┌─────────┐ ┌─────────┐ ┌─────────┐              │ │ │
38//! │  │  │  │Plugin 1 │ │Plugin 2 │ │Plugin N │              │ │ │
39//! │  │  │  │ Sandbox │ │ Sandbox │ │ Sandbox │              │ │ │
40//! │  │  │  └────┬────┘ └────┬────┘ └────┬────┘              │ │ │
41//! │  │  │       │           │           │                    │ │ │
42//! │  │  │       ▼           ▼           ▼                    │ │ │
43//! │  │  │  ┌─────────────────────────────────────────────┐  │ │ │
44//! │  │  │  │         Host Function Bridge                │  │ │ │
45//! │  │  │  │  soch_read │ soch_write │ vector_search    │  │ │ │
46//! │  │  │  └─────────────────────────────────────────────┘  │ │ │
47//! │  │  └─────────────────────────────────────────────────────┘ │ │
48//! │  └───────────────────────────────────────────────────────────┘ │
49//! └─────────────────────────────────────────────────────────────────┘
50//! ```
51//!
52//! ## Security Model
53//!
54//! Each sandbox has:
55//! - Isolated linear memory (no shared memory between plugins)
56//! - Fuel-based execution limits (prevents infinite loops)
57//! - Capability tokens (explicit permissions for each host function)
58//! - Audit logging of all host calls
59
60use std::collections::HashMap;
61use std::sync::{Arc, Mutex, RwLock};
62use std::time::{Duration, Instant};
63
64use crate::plugin_manifest::{ManifestCapabilities, PluginManifest};
65use crate::wasm_host_abi::{HostCallResult, HostFunctionContext};
66use crate::wasm_runtime::WasmPluginCapabilities;
67
68// ============================================================================
69// Sandbox Configuration
70// ============================================================================
71
72/// Configuration for the WASM sandbox runtime
73#[derive(Debug, Clone)]
74pub struct SandboxConfig {
75    /// Maximum memory per sandbox (bytes)
76    pub max_memory_bytes: usize,
77    /// Fuel limit per invocation
78    pub fuel_limit: u64,
79    /// Epoch interrupt interval
80    pub epoch_interval: Duration,
81    /// Maximum number of concurrent sandboxes
82    pub max_sandboxes: usize,
83    /// Enable detailed tracing
84    pub enable_tracing: bool,
85    /// Sandbox pool size
86    pub pool_size: usize,
87    /// Host function timeout
88    pub host_timeout: Duration,
89}
90
91impl Default for SandboxConfig {
92    fn default() -> Self {
93        Self {
94            max_memory_bytes: 16 * 1024 * 1024, // 16MB
95            fuel_limit: 1_000_000_000,
96            epoch_interval: Duration::from_millis(10),
97            max_sandboxes: 64,
98            enable_tracing: false,
99            pool_size: 4,
100            host_timeout: Duration::from_secs(30),
101        }
102    }
103}
104
105// ============================================================================
106// Sandbox Runtime
107// ============================================================================
108
109/// WASM sandbox runtime manager
110///
111/// Manages multiple isolated WASM sandboxes with:
112/// - Memory isolation between plugins
113/// - Resource limits (memory, fuel)
114/// - Capability-based access control
115/// - Connection pooling for efficiency
116pub struct WasmSandboxRuntime {
117    /// Runtime configuration
118    config: SandboxConfig,
119    /// Active sandboxes by plugin ID
120    sandboxes: RwLock<HashMap<String, Arc<PluginSandbox>>>,
121    /// Compiled modules cache (for fast instantiation)
122    module_cache: RwLock<HashMap<String, CompiledModule>>,
123    /// Global statistics
124    stats: RwLock<SandboxRuntimeStats>,
125    /// Host context provider
126    host_context: Arc<dyn HostContextProvider + Send + Sync>,
127    /// Shutdown flag
128    shutdown: Mutex<bool>,
129}
130
131/// Provider for host context (dependency injection)
132pub trait HostContextProvider: Send + Sync {
133    /// Create a host context for a plugin
134    fn create_context(
135        &self,
136        plugin_id: &str,
137        capabilities: &ManifestCapabilities,
138    ) -> HostFunctionContext;
139
140    /// Execute a read operation
141    fn read(&self, ctx: &HostFunctionContext, table_id: u32, row_id: u64) -> HostCallResult;
142
143    /// Execute a write operation
144    fn write(
145        &self,
146        ctx: &HostFunctionContext,
147        table_id: u32,
148        row_id: u64,
149        data: &[u8],
150    ) -> HostCallResult;
151
152    /// Execute vector search
153    fn vector_search(
154        &self,
155        ctx: &HostFunctionContext,
156        index: &str,
157        vector: &[f32],
158        top_k: u32,
159    ) -> HostCallResult;
160
161    /// Log a message
162    fn log(&self, ctx: &HostFunctionContext, level: u8, message: &str);
163}
164
165/// A compiled WASM module (cached for fast instantiation)
166#[derive(Clone)]
167#[allow(dead_code)]
168struct CompiledModule {
169    /// Module bytes (for recompilation if needed)
170    wasm_bytes: Vec<u8>,
171    /// Compilation timestamp
172    compiled_at: Instant,
173    /// Number of instantiations
174    instantiation_count: u64,
175    /// Hash of source for verification
176    source_hash: u64,
177}
178
179/// An isolated sandbox for a single plugin
180#[allow(dead_code)]
181pub struct PluginSandbox {
182    /// Plugin identifier
183    plugin_id: String,
184    /// Loaded manifest
185    manifest: PluginManifest,
186    /// Capabilities granted
187    capabilities: ManifestCapabilities,
188    /// Memory usage tracking
189    memory_used: Mutex<usize>,
190    /// Fuel remaining
191    fuel_remaining: Mutex<u64>,
192    /// Call count
193    call_count: Mutex<u64>,
194    /// Created at
195    created_at: Instant,
196    /// Last activity
197    last_activity: Mutex<Instant>,
198    /// Sandbox state
199    state: Mutex<SandboxState>,
200    /// Host context for this sandbox
201    host_context: HostFunctionContext,
202    /// Execution statistics
203    stats: Mutex<SandboxStats>,
204}
205
206/// Sandbox state
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum SandboxState {
209    /// Ready to execute
210    Ready,
211    /// Currently executing
212    Executing,
213    /// Suspended (waiting for async operation)
214    Suspended,
215    /// Terminated
216    Terminated,
217    /// Failed with error
218    Failed,
219}
220
221/// Statistics for a sandbox
222#[derive(Debug, Clone, Default)]
223pub struct SandboxStats {
224    /// Total invocations
225    pub total_calls: u64,
226    /// Successful invocations
227    pub successful_calls: u64,
228    /// Failed invocations
229    pub failed_calls: u64,
230    /// Total fuel consumed
231    pub fuel_consumed: u64,
232    /// Total execution time
233    pub total_execution_time: Duration,
234    /// Peak memory usage
235    pub peak_memory_bytes: usize,
236    /// Host calls made
237    pub host_calls: u64,
238    /// Host call errors
239    pub host_call_errors: u64,
240}
241
242/// Global runtime statistics
243#[derive(Debug, Clone, Default)]
244pub struct SandboxRuntimeStats {
245    /// Total sandboxes created
246    pub sandboxes_created: u64,
247    /// Active sandboxes
248    pub active_sandboxes: u64,
249    /// Total invocations across all sandboxes
250    pub total_invocations: u64,
251    /// Total fuel consumed
252    pub total_fuel_consumed: u64,
253    /// Cache hits
254    pub module_cache_hits: u64,
255    /// Cache misses
256    pub module_cache_misses: u64,
257    /// Memory violations
258    pub memory_violations: u64,
259    /// Fuel exhaustions
260    pub fuel_exhaustions: u64,
261    /// Capability denials
262    pub capability_denials: u64,
263}
264
265// ============================================================================
266// Implementation
267// ============================================================================
268
269impl WasmSandboxRuntime {
270    /// Create a new sandbox runtime
271    pub fn new(
272        config: SandboxConfig,
273        host_context: Arc<dyn HostContextProvider + Send + Sync>,
274    ) -> Self {
275        Self {
276            config,
277            sandboxes: RwLock::new(HashMap::new()),
278            module_cache: RwLock::new(HashMap::new()),
279            stats: RwLock::new(SandboxRuntimeStats::default()),
280            host_context,
281            shutdown: Mutex::new(false),
282        }
283    }
284
285    /// Load a plugin from WASM bytes
286    pub fn load_plugin(
287        &self,
288        plugin_id: &str,
289        wasm_bytes: &[u8],
290        manifest: PluginManifest,
291    ) -> Result<(), SandboxError> {
292        // Check if shutdown
293        if *self.shutdown.lock().unwrap() {
294            return Err(SandboxError::RuntimeShutdown);
295        }
296
297        // Check sandbox limit
298        let active = self.sandboxes.read().unwrap().len();
299        if active >= self.config.max_sandboxes {
300            return Err(SandboxError::TooManySandboxes {
301                current: active,
302                max: self.config.max_sandboxes,
303            });
304        }
305
306        // Validate WASM module
307        self.validate_wasm(wasm_bytes)?;
308
309        // Compile and cache module
310        let source_hash = self.compute_hash(wasm_bytes);
311        let compiled = CompiledModule {
312            wasm_bytes: wasm_bytes.to_vec(),
313            compiled_at: Instant::now(),
314            instantiation_count: 0,
315            source_hash,
316        };
317
318        self.module_cache
319            .write()
320            .unwrap()
321            .insert(plugin_id.to_string(), compiled);
322
323        // Extract capabilities from manifest
324        let capabilities = manifest.capabilities.clone();
325
326        // Create host context
327        let host_context = self.host_context.create_context(plugin_id, &capabilities);
328
329        // Create sandbox
330        let sandbox = PluginSandbox {
331            plugin_id: plugin_id.to_string(),
332            manifest,
333            capabilities,
334            memory_used: Mutex::new(0),
335            fuel_remaining: Mutex::new(self.config.fuel_limit),
336            call_count: Mutex::new(0),
337            created_at: Instant::now(),
338            last_activity: Mutex::new(Instant::now()),
339            state: Mutex::new(SandboxState::Ready),
340            host_context,
341            stats: Mutex::new(SandboxStats::default()),
342        };
343
344        self.sandboxes
345            .write()
346            .unwrap()
347            .insert(plugin_id.to_string(), Arc::new(sandbox));
348
349        // Update stats
350        let mut stats = self.stats.write().unwrap();
351        stats.sandboxes_created += 1;
352        stats.active_sandboxes += 1;
353
354        Ok(())
355    }
356
357    /// Invoke a function in a plugin sandbox
358    pub fn invoke(
359        &self,
360        plugin_id: &str,
361        function: &str,
362        args: &[SandboxValue],
363    ) -> Result<Vec<SandboxValue>, SandboxError> {
364        let sandbox = self.get_sandbox(plugin_id)?;
365
366        // Check state
367        {
368            let state = sandbox.state.lock().unwrap();
369            match *state {
370                SandboxState::Terminated => {
371                    return Err(SandboxError::SandboxTerminated(plugin_id.to_string()));
372                }
373                SandboxState::Failed => {
374                    return Err(SandboxError::SandboxFailed(plugin_id.to_string()));
375                }
376                SandboxState::Executing => {
377                    return Err(SandboxError::AlreadyExecuting(plugin_id.to_string()));
378                }
379                _ => {}
380            }
381        }
382
383        // Set state to executing
384        *sandbox.state.lock().unwrap() = SandboxState::Executing;
385        *sandbox.last_activity.lock().unwrap() = Instant::now();
386
387        let start = Instant::now();
388
389        // Execute with fuel limits
390        let result = self.execute_with_limits(&sandbox, function, args);
391
392        // Update stats
393        let elapsed = start.elapsed();
394        {
395            let mut stats = sandbox.stats.lock().unwrap();
396            stats.total_calls += 1;
397            stats.total_execution_time += elapsed;
398
399            if result.is_ok() {
400                stats.successful_calls += 1;
401            } else {
402                stats.failed_calls += 1;
403            }
404        }
405
406        *sandbox.call_count.lock().unwrap() += 1;
407
408        // Update global stats
409        {
410            let mut global_stats = self.stats.write().unwrap();
411            global_stats.total_invocations += 1;
412        }
413
414        // Restore state - always reset to ready
415        *sandbox.state.lock().unwrap() = SandboxState::Ready;
416
417        result
418    }
419
420    /// Execute with resource limits
421    fn execute_with_limits(
422        &self,
423        sandbox: &PluginSandbox,
424        function: &str,
425        args: &[SandboxValue],
426    ) -> Result<Vec<SandboxValue>, SandboxError> {
427        // Check fuel
428        let fuel_available = *sandbox.fuel_remaining.lock().unwrap();
429        if fuel_available == 0 {
430            self.stats.write().unwrap().fuel_exhaustions += 1;
431            return Err(SandboxError::FuelExhausted {
432                plugin_id: sandbox.plugin_id.clone(),
433                consumed: 0,
434            });
435        }
436
437        // Check memory
438        let memory_used = *sandbox.memory_used.lock().unwrap();
439        if memory_used > self.config.max_memory_bytes {
440            self.stats.write().unwrap().memory_violations += 1;
441            return Err(SandboxError::MemoryLimitExceeded {
442                plugin_id: sandbox.plugin_id.clone(),
443                used: memory_used,
444                limit: self.config.max_memory_bytes,
445            });
446        }
447
448        // In a real implementation, this would:
449        // 1. Get the compiled module from cache
450        // 2. Create an instance with host functions bound
451        // 3. Set up fuel metering
452        // 4. Execute the function
453        // 5. Return results
454
455        // Simulate fuel consumption based on function name length
456        let fuel_consumed = (function.len() as u64 * 1000) + (args.len() as u64 * 100);
457        *sandbox.fuel_remaining.lock().unwrap() -= fuel_consumed.min(fuel_available);
458
459        {
460            let mut stats = sandbox.stats.lock().unwrap();
461            stats.fuel_consumed += fuel_consumed.min(fuel_available);
462        }
463
464        // Simulate execution - return placeholder result
465        Ok(vec![SandboxValue::I32(0)])
466    }
467
468    /// Get a sandbox by plugin ID
469    fn get_sandbox(&self, plugin_id: &str) -> Result<Arc<PluginSandbox>, SandboxError> {
470        self.sandboxes
471            .read()
472            .unwrap()
473            .get(plugin_id)
474            .cloned()
475            .ok_or_else(|| SandboxError::PluginNotFound(plugin_id.to_string()))
476    }
477
478    /// Validate WASM module
479    fn validate_wasm(&self, wasm_bytes: &[u8]) -> Result<(), SandboxError> {
480        // Check magic number
481        if wasm_bytes.len() < 8 {
482            return Err(SandboxError::InvalidWasm("too short".to_string()));
483        }
484
485        if &wasm_bytes[0..4] != b"\0asm" {
486            return Err(SandboxError::InvalidWasm(
487                "invalid magic number".to_string(),
488            ));
489        }
490
491        // Check version
492        let version =
493            u32::from_le_bytes([wasm_bytes[4], wasm_bytes[5], wasm_bytes[6], wasm_bytes[7]]);
494        if version != 1 {
495            return Err(SandboxError::InvalidWasm(format!(
496                "unsupported version: {}",
497                version
498            )));
499        }
500
501        Ok(())
502    }
503
504    /// Compute hash of WASM bytes
505    fn compute_hash(&self, bytes: &[u8]) -> u64 {
506        use std::collections::hash_map::DefaultHasher;
507        use std::hash::{Hash, Hasher};
508
509        let mut hasher = DefaultHasher::new();
510        bytes.hash(&mut hasher);
511        hasher.finish()
512    }
513
514    /// Unload a plugin
515    pub fn unload_plugin(&self, plugin_id: &str) -> Result<(), SandboxError> {
516        let sandbox = self
517            .sandboxes
518            .write()
519            .unwrap()
520            .remove(plugin_id)
521            .ok_or_else(|| SandboxError::PluginNotFound(plugin_id.to_string()))?;
522
523        // Mark as terminated
524        *sandbox.state.lock().unwrap() = SandboxState::Terminated;
525
526        // Update stats
527        self.stats.write().unwrap().active_sandboxes -= 1;
528
529        // Remove from module cache
530        self.module_cache.write().unwrap().remove(plugin_id);
531
532        Ok(())
533    }
534
535    /// Hot-reload a plugin
536    pub fn hot_reload(
537        &self,
538        plugin_id: &str,
539        new_wasm_bytes: &[u8],
540        new_manifest: PluginManifest,
541    ) -> Result<(), SandboxError> {
542        // Validate new module
543        self.validate_wasm(new_wasm_bytes)?;
544
545        // Get current sandbox
546        let old_sandbox = self.get_sandbox(plugin_id)?;
547
548        // Wait for current execution to complete
549        loop {
550            let state = *old_sandbox.state.lock().unwrap();
551            if state != SandboxState::Executing {
552                break;
553            }
554            std::thread::sleep(Duration::from_millis(10));
555        }
556
557        // Unload old
558        self.unload_plugin(plugin_id)?;
559
560        // Load new
561        self.load_plugin(plugin_id, new_wasm_bytes, new_manifest)?;
562
563        Ok(())
564    }
565
566    /// Get plugin statistics
567    pub fn get_plugin_stats(&self, plugin_id: &str) -> Result<SandboxStats, SandboxError> {
568        let sandbox = self.get_sandbox(plugin_id)?;
569        Ok(sandbox.stats.lock().unwrap().clone())
570    }
571
572    /// Get global runtime statistics
573    pub fn get_runtime_stats(&self) -> SandboxRuntimeStats {
574        self.stats.read().unwrap().clone()
575    }
576
577    /// List all loaded plugins
578    pub fn list_plugins(&self) -> Vec<PluginInfo> {
579        self.sandboxes
580            .read()
581            .unwrap()
582            .values()
583            .map(|s| PluginInfo {
584                id: s.plugin_id.clone(),
585                name: s.manifest.plugin.name.clone(),
586                version: s.manifest.plugin.version.clone(),
587                state: *s.state.lock().unwrap(),
588                memory_used: *s.memory_used.lock().unwrap(),
589                call_count: *s.call_count.lock().unwrap(),
590                uptime: s.created_at.elapsed(),
591            })
592            .collect()
593    }
594
595    /// Reset fuel for a plugin
596    pub fn reset_fuel(&self, plugin_id: &str) -> Result<(), SandboxError> {
597        let sandbox = self.get_sandbox(plugin_id)?;
598        *sandbox.fuel_remaining.lock().unwrap() = self.config.fuel_limit;
599        Ok(())
600    }
601
602    /// Shutdown the runtime
603    pub fn shutdown(&self) {
604        *self.shutdown.lock().unwrap() = true;
605
606        // Terminate all sandboxes
607        let sandboxes: Vec<_> = self.sandboxes.read().unwrap().values().cloned().collect();
608
609        for sandbox in sandboxes {
610            *sandbox.state.lock().unwrap() = SandboxState::Terminated;
611        }
612
613        self.sandboxes.write().unwrap().clear();
614        self.module_cache.write().unwrap().clear();
615    }
616}
617
618// ============================================================================
619// Value Types
620// ============================================================================
621
622/// Value that can be passed to/from sandbox
623#[derive(Debug, Clone, PartialEq)]
624pub enum SandboxValue {
625    /// 32-bit integer
626    I32(i32),
627    /// 64-bit integer
628    I64(i64),
629    /// 32-bit float
630    F32(f32),
631    /// 64-bit float
632    F64(f64),
633    /// Byte buffer (passed via linear memory)
634    Bytes(Vec<u8>),
635    /// String (UTF-8 encoded)
636    String(String),
637}
638
639impl SandboxValue {
640    /// Get as i32
641    pub fn as_i32(&self) -> Option<i32> {
642        match self {
643            SandboxValue::I32(v) => Some(*v),
644            _ => None,
645        }
646    }
647
648    /// Get as i64
649    pub fn as_i64(&self) -> Option<i64> {
650        match self {
651            SandboxValue::I64(v) => Some(*v),
652            _ => None,
653        }
654    }
655
656    /// Get as bytes
657    pub fn as_bytes(&self) -> Option<&[u8]> {
658        match self {
659            SandboxValue::Bytes(v) => Some(v),
660            SandboxValue::String(s) => Some(s.as_bytes()),
661            _ => None,
662        }
663    }
664}
665
666// ============================================================================
667// Plugin Info
668// ============================================================================
669
670/// Information about a loaded plugin
671#[derive(Debug, Clone)]
672pub struct PluginInfo {
673    /// Plugin ID
674    pub id: String,
675    /// Plugin name from manifest
676    pub name: String,
677    /// Version
678    pub version: String,
679    /// Current state
680    pub state: SandboxState,
681    /// Memory usage in bytes
682    pub memory_used: usize,
683    /// Total calls made
684    pub call_count: u64,
685    /// Time since loading
686    pub uptime: Duration,
687}
688
689// ============================================================================
690// Errors
691// ============================================================================
692
693/// Sandbox-specific errors
694#[derive(Debug, Clone)]
695pub enum SandboxError {
696    /// Plugin not found
697    PluginNotFound(String),
698    /// Invalid WASM module
699    InvalidWasm(String),
700    /// Memory limit exceeded
701    MemoryLimitExceeded {
702        plugin_id: String,
703        used: usize,
704        limit: usize,
705    },
706    /// Fuel exhausted
707    FuelExhausted { plugin_id: String, consumed: u64 },
708    /// Capability denied
709    CapabilityDenied {
710        plugin_id: String,
711        capability: String,
712    },
713    /// Too many sandboxes
714    TooManySandboxes { current: usize, max: usize },
715    /// Sandbox terminated
716    SandboxTerminated(String),
717    /// Sandbox failed
718    SandboxFailed(String),
719    /// Already executing
720    AlreadyExecuting(String),
721    /// Runtime shutdown
722    RuntimeShutdown,
723    /// Host function error
724    HostError(String),
725    /// Execution timeout
726    Timeout {
727        plugin_id: String,
728        elapsed: Duration,
729    },
730    /// Trap occurred
731    Trap { plugin_id: String, message: String },
732}
733
734impl std::fmt::Display for SandboxError {
735    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
736        match self {
737            SandboxError::PluginNotFound(id) => write!(f, "plugin not found: {}", id),
738            SandboxError::InvalidWasm(msg) => write!(f, "invalid WASM: {}", msg),
739            SandboxError::MemoryLimitExceeded {
740                plugin_id,
741                used,
742                limit,
743            } => {
744                write!(
745                    f,
746                    "plugin {} exceeded memory limit: {} > {}",
747                    plugin_id, used, limit
748                )
749            }
750            SandboxError::FuelExhausted {
751                plugin_id,
752                consumed,
753            } => {
754                write!(f, "plugin {} exhausted fuel after {}", plugin_id, consumed)
755            }
756            SandboxError::CapabilityDenied {
757                plugin_id,
758                capability,
759            } => {
760                write!(f, "plugin {} denied capability: {}", plugin_id, capability)
761            }
762            SandboxError::TooManySandboxes { current, max } => {
763                write!(f, "too many sandboxes: {} >= {}", current, max)
764            }
765            SandboxError::SandboxTerminated(id) => write!(f, "sandbox terminated: {}", id),
766            SandboxError::SandboxFailed(id) => write!(f, "sandbox failed: {}", id),
767            SandboxError::AlreadyExecuting(id) => write!(f, "sandbox already executing: {}", id),
768            SandboxError::RuntimeShutdown => write!(f, "runtime is shutdown"),
769            SandboxError::HostError(msg) => write!(f, "host error: {}", msg),
770            SandboxError::Timeout { plugin_id, elapsed } => {
771                write!(f, "plugin {} timed out after {:?}", plugin_id, elapsed)
772            }
773            SandboxError::Trap { plugin_id, message } => {
774                write!(f, "plugin {} trapped: {}", plugin_id, message)
775            }
776        }
777    }
778}
779
780impl std::error::Error for SandboxError {}
781
782// ============================================================================
783// Host Context Provider (Default Implementation)
784// ============================================================================
785
786/// Default host context provider for testing
787pub struct DefaultHostContextProvider;
788
789impl HostContextProvider for DefaultHostContextProvider {
790    fn create_context(
791        &self,
792        plugin_id: &str,
793        capabilities: &ManifestCapabilities,
794    ) -> HostFunctionContext {
795        // Convert ManifestCapabilities to WasmPluginCapabilities
796        let wasm_caps = WasmPluginCapabilities {
797            can_read_table: capabilities.can_read_table.clone(),
798            can_write_table: capabilities.can_write_table.clone(),
799            can_vector_search: capabilities.can_vector_search,
800            can_index_search: capabilities.can_index_search,
801            can_call_plugin: capabilities.can_call_plugin.clone(),
802            memory_limit_bytes: 16 * 1024 * 1024, // 16MB default
803            fuel_limit: 1_000_000,
804            timeout_ms: 100,
805        };
806        HostFunctionContext::new(plugin_id, wasm_caps)
807    }
808
809    fn read(&self, _ctx: &HostFunctionContext, _table_id: u32, _row_id: u64) -> HostCallResult {
810        HostCallResult::Success(Vec::new())
811    }
812
813    fn write(
814        &self,
815        _ctx: &HostFunctionContext,
816        _table_id: u32,
817        _row_id: u64,
818        _data: &[u8],
819    ) -> HostCallResult {
820        HostCallResult::Ok
821    }
822
823    fn vector_search(
824        &self,
825        _ctx: &HostFunctionContext,
826        _index: &str,
827        _vector: &[f32],
828        _top_k: u32,
829    ) -> HostCallResult {
830        HostCallResult::Success(Vec::new())
831    }
832
833    fn log(&self, _ctx: &HostFunctionContext, _level: u8, _message: &str) {
834        // Default: no-op
835    }
836}
837
838// ============================================================================
839// Tests
840// ============================================================================
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    fn create_test_runtime() -> WasmSandboxRuntime {
847        WasmSandboxRuntime::new(
848            SandboxConfig::default(),
849            Arc::new(DefaultHostContextProvider),
850        )
851    }
852
853    fn create_test_manifest() -> PluginManifest {
854        PluginManifest {
855            plugin: crate::plugin_manifest::PluginMetadata {
856                name: "test-plugin".to_string(),
857                version: "1.0.0".to_string(),
858                description: "Test plugin".to_string(),
859                author: "Test Author".to_string(),
860                license: Some("MIT".to_string()),
861                homepage: None,
862                repository: None,
863                min_kernel_version: None,
864            },
865            capabilities: crate::plugin_manifest::ManifestCapabilities::default(),
866            resources: crate::plugin_manifest::ResourceLimits::default(),
867            exports: crate::plugin_manifest::ExportedFunctions::default(),
868            hooks: crate::plugin_manifest::TableHooks::default(),
869            config_schema: None,
870        }
871    }
872
873    fn create_valid_wasm() -> Vec<u8> {
874        // Minimal valid WASM module (empty module)
875        vec![
876            0x00, 0x61, 0x73, 0x6d, // Magic: \0asm
877            0x01, 0x00, 0x00, 0x00, // Version: 1
878        ]
879    }
880
881    #[test]
882    fn test_load_plugin() {
883        let runtime = create_test_runtime();
884        let manifest = create_test_manifest();
885        let wasm = create_valid_wasm();
886
887        let result = runtime.load_plugin("test", &wasm, manifest);
888        assert!(result.is_ok());
889
890        let plugins = runtime.list_plugins();
891        assert_eq!(plugins.len(), 1);
892        assert_eq!(plugins[0].id, "test");
893    }
894
895    #[test]
896    fn test_load_invalid_wasm() {
897        let runtime = create_test_runtime();
898        let manifest = create_test_manifest();
899
900        let result = runtime.load_plugin("test", b"not wasm", manifest);
901        assert!(matches!(result, Err(SandboxError::InvalidWasm(_))));
902    }
903
904    #[test]
905    fn test_unload_plugin() {
906        let runtime = create_test_runtime();
907        let manifest = create_test_manifest();
908        let wasm = create_valid_wasm();
909
910        runtime.load_plugin("test", &wasm, manifest).unwrap();
911        assert_eq!(runtime.list_plugins().len(), 1);
912
913        runtime.unload_plugin("test").unwrap();
914        assert_eq!(runtime.list_plugins().len(), 0);
915    }
916
917    #[test]
918    fn test_invoke_plugin() {
919        let runtime = create_test_runtime();
920        let manifest = create_test_manifest();
921        let wasm = create_valid_wasm();
922
923        runtime.load_plugin("test", &wasm, manifest).unwrap();
924
925        let result = runtime.invoke("test", "test_fn", &[SandboxValue::I32(42)]);
926        assert!(result.is_ok());
927    }
928
929    #[test]
930    fn test_invoke_nonexistent() {
931        let runtime = create_test_runtime();
932
933        let result = runtime.invoke("nonexistent", "fn", &[]);
934        assert!(matches!(result, Err(SandboxError::PluginNotFound(_))));
935    }
936
937    #[test]
938    fn test_sandbox_limit() {
939        let config = SandboxConfig {
940            max_sandboxes: 2,
941            ..Default::default()
942        };
943        let runtime = WasmSandboxRuntime::new(config, Arc::new(DefaultHostContextProvider));
944        let wasm = create_valid_wasm();
945
946        runtime
947            .load_plugin("p1", &wasm, create_test_manifest())
948            .unwrap();
949        runtime
950            .load_plugin("p2", &wasm, create_test_manifest())
951            .unwrap();
952
953        let result = runtime.load_plugin("p3", &wasm, create_test_manifest());
954        assert!(matches!(result, Err(SandboxError::TooManySandboxes { .. })));
955    }
956
957    #[test]
958    fn test_runtime_stats() {
959        let runtime = create_test_runtime();
960        let manifest = create_test_manifest();
961        let wasm = create_valid_wasm();
962
963        runtime.load_plugin("test", &wasm, manifest).unwrap();
964        runtime.invoke("test", "fn1", &[]).unwrap();
965        runtime.invoke("test", "fn2", &[]).unwrap();
966
967        let stats = runtime.get_runtime_stats();
968        assert_eq!(stats.sandboxes_created, 1);
969        assert_eq!(stats.active_sandboxes, 1);
970        assert_eq!(stats.total_invocations, 2);
971    }
972
973    #[test]
974    fn test_plugin_stats() {
975        let runtime = create_test_runtime();
976        let manifest = create_test_manifest();
977        let wasm = create_valid_wasm();
978
979        runtime.load_plugin("test", &wasm, manifest).unwrap();
980        runtime.invoke("test", "fn", &[]).unwrap();
981
982        let stats = runtime.get_plugin_stats("test").unwrap();
983        assert_eq!(stats.total_calls, 1);
984        assert_eq!(stats.successful_calls, 1);
985    }
986
987    #[test]
988    fn test_hot_reload() {
989        let runtime = create_test_runtime();
990        let manifest = create_test_manifest();
991        let wasm = create_valid_wasm();
992
993        runtime
994            .load_plugin("test", &wasm, manifest.clone())
995            .unwrap();
996
997        // Reload with new module
998        let result = runtime.hot_reload("test", &wasm, manifest);
999        assert!(result.is_ok());
1000
1001        // Stats should be reset
1002        let stats = runtime.get_plugin_stats("test").unwrap();
1003        assert_eq!(stats.total_calls, 0);
1004    }
1005
1006    #[test]
1007    fn test_reset_fuel() {
1008        let runtime = create_test_runtime();
1009        let manifest = create_test_manifest();
1010        let wasm = create_valid_wasm();
1011
1012        runtime.load_plugin("test", &wasm, manifest).unwrap();
1013
1014        // Consume some fuel
1015        runtime.invoke("test", "some_function", &[]).unwrap();
1016
1017        // Reset fuel
1018        runtime.reset_fuel("test").unwrap();
1019
1020        // Should be able to invoke again
1021        let result = runtime.invoke("test", "fn", &[]);
1022        assert!(result.is_ok());
1023    }
1024
1025    #[test]
1026    fn test_shutdown() {
1027        let runtime = create_test_runtime();
1028        let manifest = create_test_manifest();
1029        let wasm = create_valid_wasm();
1030
1031        runtime.load_plugin("test", &wasm, manifest).unwrap();
1032        runtime.shutdown();
1033
1034        assert_eq!(runtime.list_plugins().len(), 0);
1035
1036        let result = runtime.load_plugin("new", &wasm, create_test_manifest());
1037        assert!(matches!(result, Err(SandboxError::RuntimeShutdown)));
1038    }
1039
1040    #[test]
1041    fn test_sandbox_value() {
1042        let v1 = SandboxValue::I32(42);
1043        assert_eq!(v1.as_i32(), Some(42));
1044        assert_eq!(v1.as_i64(), None);
1045
1046        let v2 = SandboxValue::String("hello".to_string());
1047        assert_eq!(v2.as_bytes(), Some(b"hello".as_slice()));
1048    }
1049}