Skip to main content

symbi_runtime/integrations/
sandbox_orchestrator.rs

1//! Sandbox Orchestrator Integration Interface
2//!
3//! Provides interface for integrating with multi-tier sandboxing systems
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::time::{Duration, SystemTime};
10
11use crate::sandbox::{ExecutionResult, SandboxRunner, SandboxTier};
12use crate::types::*;
13use std::sync::Arc;
14
15/// Maximum bytes for stdout/stderr capture from sandbox commands.
16const MAX_STDOUT_BYTES: usize = 10 * 1024 * 1024; // 10 MB
17/// Maximum bytes for file download from sandbox.
18const MAX_FILE_DOWNLOAD_BYTES: usize = 50 * 1024 * 1024; // 50 MB
19
20/// Sandbox orchestrator trait for managing agent sandboxes
21#[async_trait]
22pub trait SandboxOrchestrator: Send + Sync {
23    /// Create a new sandbox for an agent
24    async fn create_sandbox(&self, request: SandboxRequest) -> Result<SandboxInfo, SandboxError>;
25
26    /// Start a sandbox
27    async fn start_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError>;
28
29    /// Stop a sandbox
30    async fn stop_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError>;
31
32    /// Destroy a sandbox and cleanup resources
33    async fn destroy_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError>;
34
35    /// Get sandbox status and information
36    async fn get_sandbox_info(&self, sandbox_id: SandboxId) -> Result<SandboxInfo, SandboxError>;
37
38    /// List all sandboxes
39    async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>, SandboxError>;
40
41    /// Execute a command in a sandbox
42    async fn execute_command(
43        &self,
44        sandbox_id: SandboxId,
45        command: SandboxCommand,
46    ) -> Result<CommandResult, SandboxError>;
47
48    /// Upload files to a sandbox
49    async fn upload_files(
50        &self,
51        sandbox_id: SandboxId,
52        files: Vec<FileUpload>,
53    ) -> Result<(), SandboxError>;
54
55    /// Download files from a sandbox
56    async fn download_files(
57        &self,
58        sandbox_id: SandboxId,
59        paths: Vec<String>,
60    ) -> Result<Vec<FileDownload>, SandboxError>;
61
62    /// Get sandbox resource usage
63    async fn get_resource_usage(
64        &self,
65        sandbox_id: SandboxId,
66    ) -> Result<SandboxResourceUsage, SandboxError>;
67
68    /// Update sandbox configuration
69    async fn update_sandbox(
70        &self,
71        sandbox_id: SandboxId,
72        config: SandboxConfig,
73    ) -> Result<(), SandboxError>;
74
75    /// Get sandbox logs
76    async fn get_logs(
77        &self,
78        sandbox_id: SandboxId,
79        options: LogOptions,
80    ) -> Result<Vec<LogEntry>, SandboxError>;
81
82    /// Create a snapshot of a sandbox
83    async fn create_snapshot(
84        &self,
85        sandbox_id: SandboxId,
86        name: String,
87    ) -> Result<SnapshotId, SandboxError>;
88
89    /// Restore sandbox from snapshot
90    async fn restore_snapshot(
91        &self,
92        sandbox_id: SandboxId,
93        snapshot_id: SnapshotId,
94    ) -> Result<(), SandboxError>;
95
96    /// Delete a snapshot
97    async fn delete_snapshot(&self, snapshot_id: SnapshotId) -> Result<(), SandboxError>;
98
99    /// Execute code using a specific sandbox tier
100    async fn execute_code(
101        &self,
102        tier: SandboxTier,
103        code: &str,
104        env: HashMap<String, String>,
105    ) -> Result<ExecutionResult, SandboxError>;
106
107    /// Register a sandbox runner for a specific tier
108    async fn register_sandbox_runner(
109        &self,
110        tier: SandboxTier,
111        runner: Arc<dyn SandboxRunner>,
112    ) -> Result<(), SandboxError>;
113}
114
115/// Sandbox creation request
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SandboxRequest {
118    pub agent_id: AgentId,
119    pub sandbox_type: SandboxType,
120    pub config: SandboxConfig,
121    pub security_level: SecurityTier,
122    pub resource_limits: ResourceLimits,
123    pub network_config: NetworkConfig,
124    pub storage_config: StorageConfig,
125    pub metadata: HashMap<String, String>,
126}
127
128/// Sandbox types for different isolation levels
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub enum SandboxType {
131    /// Docker container sandbox
132    Docker { image: String, tag: String },
133    /// gVisor sandbox for enhanced security (requires enterprise feature)
134    #[cfg(feature = "enterprise")]
135    GVisor { runtime: String, platform: String },
136    /// Firecracker microVM sandbox (requires enterprise feature)
137    #[cfg(feature = "enterprise")]
138    Firecracker {
139        kernel_image: String,
140        rootfs_image: String,
141    },
142    /// Process-level sandbox
143    Process {
144        executable: String,
145        working_dir: PathBuf,
146    },
147    /// Custom sandbox implementation
148    Custom {
149        provider: String,
150        config: HashMap<String, String>,
151    },
152}
153
154/// Sandbox configuration
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct SandboxConfig {
157    pub name: String,
158    pub description: String,
159    pub environment_variables: HashMap<String, String>,
160    pub working_directory: Option<PathBuf>,
161    pub command: Option<Vec<String>>,
162    pub entrypoint: Option<Vec<String>>,
163    pub user: Option<String>,
164    pub group: Option<String>,
165    pub capabilities: Vec<String>,
166    pub security_options: SecurityOptions,
167    pub auto_remove: bool,
168    pub restart_policy: RestartPolicy,
169    pub health_check: Option<HealthCheck>,
170}
171
172/// Security options for sandbox
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct SecurityOptions {
175    pub read_only_root: bool,
176    pub no_new_privileges: bool,
177    pub seccomp_profile: Option<String>,
178    pub apparmor_profile: Option<String>,
179    pub selinux_label: Option<String>,
180    pub privileged: bool,
181    pub drop_capabilities: Vec<String>,
182    pub add_capabilities: Vec<String>,
183}
184
185/// Restart policy for sandbox
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub enum RestartPolicy {
188    Never,
189    Always,
190    OnFailure { max_retries: u32 },
191    UnlessStopped,
192}
193
194/// Health check configuration
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct HealthCheck {
197    pub command: Vec<String>,
198    pub interval: Duration,
199    pub timeout: Duration,
200    pub retries: u32,
201    pub start_period: Duration,
202}
203
204/// Network configuration for sandbox
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct NetworkConfig {
207    pub mode: NetworkMode,
208    pub ports: Vec<PortMapping>,
209    pub dns_servers: Vec<String>,
210    pub dns_search: Vec<String>,
211    pub hostname: Option<String>,
212    pub extra_hosts: HashMap<String, String>,
213    pub network_aliases: Vec<String>,
214}
215
216/// Network modes for sandbox
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub enum NetworkMode {
219    Bridge,
220    Host,
221    None,
222    Container { container_id: String },
223    Custom { network_name: String },
224}
225
226/// Port mapping for network access
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct PortMapping {
229    pub host_port: u16,
230    pub container_port: u16,
231    pub protocol: Protocol,
232    pub host_ip: Option<String>,
233}
234
235/// Network protocols
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub enum Protocol {
238    TCP,
239    UDP,
240    SCTP,
241}
242
243/// Storage configuration for sandbox
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct StorageConfig {
246    pub volumes: Vec<VolumeMount>,
247    pub tmpfs_mounts: Vec<TmpfsMount>,
248    pub storage_driver: Option<String>,
249    pub storage_options: HashMap<String, String>,
250}
251
252/// Volume mount configuration
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct VolumeMount {
255    pub source: String,
256    pub target: String,
257    pub mount_type: MountType,
258    pub read_only: bool,
259    pub options: Vec<String>,
260}
261
262/// Mount types for volumes
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub enum MountType {
265    Bind,
266    Volume,
267    Tmpfs,
268}
269
270/// Tmpfs mount configuration
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct TmpfsMount {
273    pub target: String,
274    pub size: Option<u64>,
275    pub mode: Option<u32>,
276    pub options: Vec<String>,
277}
278
279/// Sandbox information and status
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct SandboxInfo {
282    pub id: SandboxId,
283    pub agent_id: AgentId,
284    pub sandbox_type: SandboxType,
285    pub status: SandboxStatus,
286    pub config: SandboxConfig,
287    pub resource_usage: SandboxResourceUsage,
288    pub network_info: NetworkInfo,
289    pub created_at: SystemTime,
290    pub started_at: Option<SystemTime>,
291    pub stopped_at: Option<SystemTime>,
292    pub metadata: HashMap<String, String>,
293}
294
295/// Sandbox status
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297pub enum SandboxStatus {
298    Creating,
299    Created,
300    Starting,
301    Running,
302    Stopping,
303    Stopped,
304    Paused,
305    Error { message: String },
306    Destroyed,
307}
308
309/// Network information for sandbox
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct NetworkInfo {
312    pub ip_address: Option<String>,
313    pub mac_address: Option<String>,
314    pub gateway: Option<String>,
315    pub bridge: Option<String>,
316    pub ports: Vec<PortMapping>,
317}
318
319/// Command to execute in sandbox
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct SandboxCommand {
322    pub command: Vec<String>,
323    pub working_dir: Option<PathBuf>,
324    pub environment: HashMap<String, String>,
325    pub user: Option<String>,
326    pub timeout: Option<Duration>,
327    pub stdin: Option<String>,
328}
329
330/// Result of a command execution in a sandbox.
331///
332/// **Security**: stdout and stderr are capped at 10 MB to prevent OOM from
333/// runaway commands (e.g. `cat /dev/urandom`). Callers should use streaming
334/// interfaces for large outputs.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct CommandResult {
337    pub exit_code: i32,
338    pub stdout: String,
339    pub stderr: String,
340    pub execution_time: Duration,
341    pub timed_out: bool,
342}
343
344/// File upload to sandbox
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct FileUpload {
347    pub local_path: PathBuf,
348    pub sandbox_path: String,
349    pub permissions: Option<u32>,
350    pub owner: Option<String>,
351    pub group: Option<String>,
352}
353
354/// File download from sandbox.
355///
356/// **Security**: content is capped at 50 MB. Use streaming download for larger files.
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct FileDownload {
359    pub sandbox_path: String,
360    pub content: Vec<u8>,
361    pub permissions: u32,
362    pub size: u64,
363    pub modified_at: SystemTime,
364}
365
366/// Sandbox resource usage
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct SandboxResourceUsage {
369    pub cpu_usage: CpuUsage,
370    pub memory_usage: MemoryUsage,
371    pub disk_usage: DiskUsage,
372    pub network_usage: NetworkUsage,
373    pub timestamp: SystemTime,
374}
375
376/// CPU usage statistics
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct CpuUsage {
379    pub total_usage: Duration,
380    pub user_usage: Duration,
381    pub system_usage: Duration,
382    pub cpu_percent: f64,
383    pub throttled_time: Duration,
384}
385
386/// Memory usage statistics
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct MemoryUsage {
389    pub current: u64,
390    pub peak: u64,
391    pub limit: u64,
392    pub cache: u64,
393    pub swap: u64,
394    pub percent: f64,
395}
396
397/// Disk usage statistics
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct DiskUsage {
400    pub read_bytes: u64,
401    pub write_bytes: u64,
402    pub read_ops: u64,
403    pub write_ops: u64,
404    pub total_space: u64,
405    pub used_space: u64,
406}
407
408/// Network usage statistics
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct NetworkUsage {
411    pub rx_bytes: u64,
412    pub tx_bytes: u64,
413    pub rx_packets: u64,
414    pub tx_packets: u64,
415    pub rx_errors: u64,
416    pub tx_errors: u64,
417}
418
419/// Log options for retrieving sandbox logs
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct LogOptions {
422    pub since: Option<SystemTime>,
423    pub until: Option<SystemTime>,
424    pub tail: Option<u32>,
425    pub follow: bool,
426    pub timestamps: bool,
427    pub details: bool,
428}
429
430/// Log entry from sandbox
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct LogEntry {
433    pub timestamp: SystemTime,
434    pub level: LogLevel,
435    pub source: LogSource,
436    pub message: String,
437    pub metadata: HashMap<String, String>,
438}
439
440/// Log levels
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub enum LogLevel {
443    Trace,
444    Debug,
445    Info,
446    Warning,
447    Error,
448    Fatal,
449}
450
451/// Log sources
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub enum LogSource {
454    Stdout,
455    Stderr,
456    System,
457    Application,
458}
459
460/// Sandbox identifier
461pub type SandboxId = uuid::Uuid;
462
463/// Snapshot identifier
464pub type SnapshotId = uuid::Uuid;
465
466/// Mock sandbox orchestrator for testing and development
467pub struct MockSandboxOrchestrator {
468    sandboxes: dashmap::DashMap<SandboxId, SandboxInfo>,
469    snapshots: dashmap::DashMap<SnapshotId, SandboxSnapshot>,
470    sandbox_runners: dashmap::DashMap<SandboxTier, Arc<dyn SandboxRunner>>,
471}
472
473/// Snapshot information
474#[derive(Debug, Clone)]
475struct SandboxSnapshot {
476    id: SnapshotId,
477    sandbox_id: SandboxId,
478    name: String,
479    created_at: SystemTime,
480    size: u64,
481}
482
483impl SandboxSnapshot {
484    fn new(id: SnapshotId, sandbox_id: SandboxId, name: String) -> Self {
485        Self {
486            id,
487            sandbox_id,
488            name,
489            created_at: SystemTime::now(),
490            size: 0,
491        }
492    }
493
494    fn get_id(&self) -> SnapshotId {
495        self.id
496    }
497
498    fn get_sandbox_id(&self) -> SandboxId {
499        self.sandbox_id
500    }
501
502    fn get_name(&self) -> &str {
503        &self.name
504    }
505
506    fn get_age(&self) -> Duration {
507        SystemTime::now()
508            .duration_since(self.created_at)
509            .unwrap_or_default()
510    }
511
512    fn get_size(&self) -> u64 {
513        self.size
514    }
515
516    fn set_size(&mut self, size: u64) {
517        self.size = size;
518    }
519
520    fn is_expired(&self, max_age: Duration) -> bool {
521        self.get_age() > max_age
522    }
523}
524
525impl MockSandboxOrchestrator {
526    pub fn new() -> Self {
527        Self {
528            sandboxes: dashmap::DashMap::new(),
529            snapshots: dashmap::DashMap::new(),
530            sandbox_runners: dashmap::DashMap::new(),
531        }
532    }
533
534    fn create_mock_resource_usage() -> SandboxResourceUsage {
535        SandboxResourceUsage {
536            cpu_usage: CpuUsage {
537                total_usage: Duration::from_secs(10),
538                user_usage: Duration::from_secs(8),
539                system_usage: Duration::from_secs(2),
540                cpu_percent: 5.0,
541                throttled_time: Duration::from_millis(0),
542            },
543            memory_usage: MemoryUsage {
544                current: 64 * 1024 * 1024, // 64MB
545                peak: 128 * 1024 * 1024,   // 128MB
546                limit: 512 * 1024 * 1024,  // 512MB
547                cache: 16 * 1024 * 1024,   // 16MB
548                swap: 0,
549                percent: 12.5,
550            },
551            disk_usage: DiskUsage {
552                read_bytes: 1024 * 1024, // 1MB
553                write_bytes: 512 * 1024, // 512KB
554                read_ops: 100,
555                write_ops: 50,
556                total_space: 10 * 1024 * 1024 * 1024, // 10GB
557                used_space: 1024 * 1024 * 1024,       // 1GB
558            },
559            network_usage: NetworkUsage {
560                rx_bytes: 2048,
561                tx_bytes: 1024,
562                rx_packets: 20,
563                tx_packets: 15,
564                rx_errors: 0,
565                tx_errors: 0,
566            },
567            timestamp: SystemTime::now(),
568        }
569    }
570}
571
572impl Default for MockSandboxOrchestrator {
573    fn default() -> Self {
574        Self::new()
575    }
576}
577
578#[async_trait]
579impl SandboxOrchestrator for MockSandboxOrchestrator {
580    async fn create_sandbox(&self, request: SandboxRequest) -> Result<SandboxInfo, SandboxError> {
581        let sandbox_id = SandboxId::new_v4();
582        let now = SystemTime::now();
583
584        let sandbox_info = SandboxInfo {
585            id: sandbox_id,
586            agent_id: request.agent_id,
587            sandbox_type: request.sandbox_type,
588            status: SandboxStatus::Created,
589            config: request.config,
590            resource_usage: Self::create_mock_resource_usage(),
591            network_info: NetworkInfo {
592                ip_address: Some("172.17.0.2".to_string()),
593                mac_address: Some("02:42:ac:11:00:02".to_string()),
594                gateway: Some("172.17.0.1".to_string()),
595                bridge: Some("docker0".to_string()),
596                ports: request.network_config.ports,
597            },
598            created_at: now,
599            started_at: None,
600            stopped_at: None,
601            metadata: request.metadata,
602        };
603
604        self.sandboxes.insert(sandbox_id, sandbox_info.clone());
605        Ok(sandbox_info)
606    }
607
608    async fn start_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError> {
609        if let Some(mut sandbox) = self.sandboxes.get_mut(&sandbox_id) {
610            sandbox.status = SandboxStatus::Running;
611            sandbox.started_at = Some(SystemTime::now());
612            Ok(())
613        } else {
614            Err(SandboxError::SandboxNotFound {
615                id: sandbox_id.to_string(),
616            })
617        }
618    }
619
620    async fn stop_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError> {
621        if let Some(mut sandbox) = self.sandboxes.get_mut(&sandbox_id) {
622            sandbox.status = SandboxStatus::Stopped;
623            sandbox.stopped_at = Some(SystemTime::now());
624            Ok(())
625        } else {
626            Err(SandboxError::SandboxNotFound {
627                id: sandbox_id.to_string(),
628            })
629        }
630    }
631
632    async fn destroy_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError> {
633        if let Some(mut sandbox) = self.sandboxes.get_mut(&sandbox_id) {
634            sandbox.status = SandboxStatus::Destroyed;
635            Ok(())
636        } else {
637            Err(SandboxError::SandboxNotFound {
638                id: sandbox_id.to_string(),
639            })
640        }
641    }
642
643    async fn get_sandbox_info(&self, sandbox_id: SandboxId) -> Result<SandboxInfo, SandboxError> {
644        self.sandboxes
645            .get(&sandbox_id)
646            .map(|r| r.clone())
647            .ok_or(SandboxError::SandboxNotFound {
648                id: sandbox_id.to_string(),
649            })
650    }
651
652    async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>, SandboxError> {
653        Ok(self.sandboxes.iter().map(|r| r.value().clone()).collect())
654    }
655
656    async fn execute_command(
657        &self,
658        sandbox_id: SandboxId,
659        command: SandboxCommand,
660    ) -> Result<CommandResult, SandboxError> {
661        if self.sandboxes.contains_key(&sandbox_id) {
662            // Mock command execution
663            let stdout = format!("Mock output for command: {:?}", command.command);
664            let stderr = String::new();
665
666            // Truncate stdout/stderr to MAX_STDOUT_BYTES
667            let stdout = if stdout.len() > MAX_STDOUT_BYTES {
668                format!(
669                    "{}... [truncated at {} bytes]",
670                    &stdout[..MAX_STDOUT_BYTES],
671                    MAX_STDOUT_BYTES
672                )
673            } else {
674                stdout
675            };
676            let stderr = if stderr.len() > MAX_STDOUT_BYTES {
677                format!(
678                    "{}... [truncated at {} bytes]",
679                    &stderr[..MAX_STDOUT_BYTES],
680                    MAX_STDOUT_BYTES
681                )
682            } else {
683                stderr
684            };
685
686            Ok(CommandResult {
687                exit_code: 0,
688                stdout,
689                stderr,
690                execution_time: Duration::from_millis(100),
691                timed_out: false,
692            })
693        } else {
694            Err(SandboxError::SandboxNotFound {
695                id: sandbox_id.to_string(),
696            })
697        }
698    }
699
700    async fn upload_files(
701        &self,
702        sandbox_id: SandboxId,
703        _files: Vec<FileUpload>,
704    ) -> Result<(), SandboxError> {
705        if self.sandboxes.contains_key(&sandbox_id) {
706            Ok(())
707        } else {
708            Err(SandboxError::SandboxNotFound {
709                id: sandbox_id.to_string(),
710            })
711        }
712    }
713
714    async fn download_files(
715        &self,
716        sandbox_id: SandboxId,
717        paths: Vec<String>,
718    ) -> Result<Vec<FileDownload>, SandboxError> {
719        if self.sandboxes.contains_key(&sandbox_id) {
720            let downloads = paths
721                .into_iter()
722                .map(|path| {
723                    let content = b"mock file content".to_vec();
724                    // Truncate content to MAX_FILE_DOWNLOAD_BYTES
725                    let content = if content.len() > MAX_FILE_DOWNLOAD_BYTES {
726                        content[..MAX_FILE_DOWNLOAD_BYTES].to_vec()
727                    } else {
728                        content
729                    };
730                    let size = content.len() as u64;
731                    FileDownload {
732                        sandbox_path: path,
733                        content,
734                        permissions: 0o644,
735                        size,
736                        modified_at: SystemTime::now(),
737                    }
738                })
739                .collect();
740            Ok(downloads)
741        } else {
742            Err(SandboxError::SandboxNotFound {
743                id: sandbox_id.to_string(),
744            })
745        }
746    }
747
748    async fn get_resource_usage(
749        &self,
750        sandbox_id: SandboxId,
751    ) -> Result<SandboxResourceUsage, SandboxError> {
752        if self.sandboxes.contains_key(&sandbox_id) {
753            Ok(Self::create_mock_resource_usage())
754        } else {
755            Err(SandboxError::SandboxNotFound {
756                id: sandbox_id.to_string(),
757            })
758        }
759    }
760
761    async fn update_sandbox(
762        &self,
763        sandbox_id: SandboxId,
764        config: SandboxConfig,
765    ) -> Result<(), SandboxError> {
766        if let Some(mut sandbox) = self.sandboxes.get_mut(&sandbox_id) {
767            sandbox.config = config;
768            Ok(())
769        } else {
770            Err(SandboxError::SandboxNotFound {
771                id: sandbox_id.to_string(),
772            })
773        }
774    }
775
776    async fn get_logs(
777        &self,
778        sandbox_id: SandboxId,
779        _options: LogOptions,
780    ) -> Result<Vec<LogEntry>, SandboxError> {
781        if self.sandboxes.contains_key(&sandbox_id) {
782            Ok(vec![LogEntry {
783                timestamp: SystemTime::now(),
784                level: LogLevel::Info,
785                source: LogSource::Stdout,
786                message: "Mock log entry".to_string(),
787                metadata: HashMap::new(),
788            }])
789        } else {
790            Err(SandboxError::SandboxNotFound {
791                id: sandbox_id.to_string(),
792            })
793        }
794    }
795
796    async fn create_snapshot(
797        &self,
798        sandbox_id: SandboxId,
799        name: String,
800    ) -> Result<SnapshotId, SandboxError> {
801        if self.sandboxes.contains_key(&sandbox_id) {
802            let snapshot_id = SnapshotId::new_v4();
803            let mut snapshot = SandboxSnapshot::new(snapshot_id, sandbox_id, name);
804            snapshot.set_size(1024 * 1024 * 100); // 100MB
805
806            tracing::info!(
807                "Created snapshot {} for sandbox {} with size {} bytes",
808                snapshot.get_id(),
809                snapshot.get_sandbox_id(),
810                snapshot.get_size()
811            );
812
813            self.snapshots.insert(snapshot_id, snapshot);
814            Ok(snapshot_id)
815        } else {
816            Err(SandboxError::SandboxNotFound {
817                id: sandbox_id.to_string(),
818            })
819        }
820    }
821
822    async fn restore_snapshot(
823        &self,
824        sandbox_id: SandboxId,
825        snapshot_id: SnapshotId,
826    ) -> Result<(), SandboxError> {
827        if !self.sandboxes.contains_key(&sandbox_id) {
828            return Err(SandboxError::SandboxNotFound {
829                id: sandbox_id.to_string(),
830            });
831        }
832
833        if !self.snapshots.contains_key(&snapshot_id) {
834            return Err(SandboxError::SnapshotNotFound {
835                id: snapshot_id.to_string(),
836            });
837        }
838
839        Ok(())
840    }
841
842    async fn delete_snapshot(&self, snapshot_id: SnapshotId) -> Result<(), SandboxError> {
843        if let Some(snapshot) = self.snapshots.get(&snapshot_id) {
844            tracing::info!(
845                "Deleting snapshot '{}' (age: {:?}s, size: {} bytes)",
846                snapshot.get_name(),
847                snapshot.get_age().as_secs(),
848                snapshot.get_size()
849            );
850        }
851
852        if self.snapshots.remove(&snapshot_id).is_some() {
853            Ok(())
854        } else {
855            Err(SandboxError::SnapshotNotFound {
856                id: snapshot_id.to_string(),
857            })
858        }
859    }
860
861    async fn execute_code(
862        &self,
863        tier: SandboxTier,
864        code: &str,
865        env: HashMap<String, String>,
866    ) -> Result<ExecutionResult, SandboxError> {
867        let runner = self.sandbox_runners.get(&tier).map(|r| r.value().clone());
868
869        if let Some(runner) = runner {
870            runner
871                .execute(code, env)
872                .await
873                .map_err(|e| SandboxError::ExecutionFailed(format!("Code execution failed: {}", e)))
874        } else {
875            Err(SandboxError::UnsupportedTier(format!("{:?}", tier)))
876        }
877    }
878
879    async fn register_sandbox_runner(
880        &self,
881        tier: SandboxTier,
882        runner: Arc<dyn SandboxRunner>,
883    ) -> Result<(), SandboxError> {
884        self.sandbox_runners.insert(tier, runner);
885        Ok(())
886    }
887}
888
889impl MockSandboxOrchestrator {
890    /// Clean up expired snapshots
891    pub async fn cleanup_expired_snapshots(&self, max_age: Duration) -> u32 {
892        let expired_ids: Vec<SnapshotId> = self
893            .snapshots
894            .iter()
895            .filter_map(|entry| {
896                if entry.value().is_expired(max_age) {
897                    tracing::info!(
898                        "Snapshot '{}' expired (age: {:?})",
899                        entry.value().get_name(),
900                        entry.value().get_age()
901                    );
902                    Some(*entry.key())
903                } else {
904                    None
905                }
906            })
907            .collect();
908
909        let expired_count = expired_ids.len() as u32;
910        for id in expired_ids {
911            self.snapshots.remove(&id);
912        }
913
914        expired_count
915    }
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921
922    #[tokio::test]
923    async fn test_sandbox_lifecycle() {
924        let orchestrator = MockSandboxOrchestrator::new();
925        let agent_id = AgentId::new();
926
927        // Create sandbox
928        let request = SandboxRequest {
929            agent_id,
930            sandbox_type: SandboxType::Docker {
931                image: "ubuntu".to_string(),
932                tag: "latest".to_string(),
933            },
934            config: SandboxConfig {
935                name: "test-sandbox".to_string(),
936                description: "Test sandbox".to_string(),
937                environment_variables: HashMap::new(),
938                working_directory: None,
939                command: None,
940                entrypoint: None,
941                user: None,
942                group: None,
943                capabilities: vec![],
944                security_options: SecurityOptions {
945                    read_only_root: false,
946                    no_new_privileges: true,
947                    seccomp_profile: None,
948                    apparmor_profile: None,
949                    selinux_label: None,
950                    privileged: false,
951                    drop_capabilities: vec![],
952                    add_capabilities: vec![],
953                },
954                auto_remove: true,
955                restart_policy: RestartPolicy::Never,
956                health_check: None,
957            },
958            security_level: SecurityTier::Tier2,
959            resource_limits: ResourceLimits {
960                memory_mb: 512,
961                cpu_cores: 2.0,
962                disk_io_mbps: 100,
963                network_io_mbps: 10,
964                execution_timeout: std::time::Duration::from_secs(300),
965                idle_timeout: std::time::Duration::from_secs(60),
966            },
967            network_config: NetworkConfig {
968                mode: NetworkMode::Bridge,
969                ports: vec![],
970                dns_servers: vec![],
971                dns_search: vec![],
972                hostname: None,
973                extra_hosts: HashMap::new(),
974                network_aliases: vec![],
975            },
976            storage_config: StorageConfig {
977                volumes: vec![],
978                tmpfs_mounts: vec![],
979                storage_driver: None,
980                storage_options: HashMap::new(),
981            },
982            metadata: HashMap::new(),
983        };
984
985        let sandbox_info = orchestrator.create_sandbox(request).await.unwrap();
986        assert_eq!(sandbox_info.status, SandboxStatus::Created);
987
988        // Start sandbox
989        orchestrator.start_sandbox(sandbox_info.id).await.unwrap();
990        let updated_info = orchestrator
991            .get_sandbox_info(sandbox_info.id)
992            .await
993            .unwrap();
994        assert_eq!(updated_info.status, SandboxStatus::Running);
995
996        // Stop sandbox
997        orchestrator.stop_sandbox(sandbox_info.id).await.unwrap();
998        let stopped_info = orchestrator
999            .get_sandbox_info(sandbox_info.id)
1000            .await
1001            .unwrap();
1002        assert_eq!(stopped_info.status, SandboxStatus::Stopped);
1003    }
1004
1005    #[tokio::test]
1006    async fn test_command_execution() {
1007        let orchestrator = MockSandboxOrchestrator::new();
1008        let agent_id = AgentId::new();
1009
1010        let request = SandboxRequest {
1011            agent_id,
1012            sandbox_type: SandboxType::Docker {
1013                image: "ubuntu".to_string(),
1014                tag: "latest".to_string(),
1015            },
1016            config: SandboxConfig {
1017                name: "test-sandbox".to_string(),
1018                description: "Test sandbox".to_string(),
1019                environment_variables: HashMap::new(),
1020                working_directory: None,
1021                command: None,
1022                entrypoint: None,
1023                user: None,
1024                group: None,
1025                capabilities: vec![],
1026                security_options: SecurityOptions {
1027                    read_only_root: false,
1028                    no_new_privileges: true,
1029                    seccomp_profile: None,
1030                    apparmor_profile: None,
1031                    selinux_label: None,
1032                    privileged: false,
1033                    drop_capabilities: vec![],
1034                    add_capabilities: vec![],
1035                },
1036                auto_remove: true,
1037                restart_policy: RestartPolicy::Never,
1038                health_check: None,
1039            },
1040            security_level: SecurityTier::Tier2,
1041            resource_limits: ResourceLimits {
1042                memory_mb: 512,
1043                cpu_cores: 2.0,
1044                disk_io_mbps: 100,
1045                network_io_mbps: 10,
1046                execution_timeout: std::time::Duration::from_secs(300),
1047                idle_timeout: std::time::Duration::from_secs(60),
1048            },
1049            network_config: NetworkConfig {
1050                mode: NetworkMode::Bridge,
1051                ports: vec![],
1052                dns_servers: vec![],
1053                dns_search: vec![],
1054                hostname: None,
1055                extra_hosts: HashMap::new(),
1056                network_aliases: vec![],
1057            },
1058            storage_config: StorageConfig {
1059                volumes: vec![],
1060                tmpfs_mounts: vec![],
1061                storage_driver: None,
1062                storage_options: HashMap::new(),
1063            },
1064            metadata: HashMap::new(),
1065        };
1066
1067        let sandbox_info = orchestrator.create_sandbox(request).await.unwrap();
1068
1069        let command = SandboxCommand {
1070            command: vec!["echo".to_string(), "hello".to_string()],
1071            working_dir: None,
1072            environment: HashMap::new(),
1073            user: None,
1074            timeout: None,
1075            stdin: None,
1076        };
1077
1078        let result = orchestrator
1079            .execute_command(sandbox_info.id, command)
1080            .await
1081            .unwrap();
1082        assert_eq!(result.exit_code, 0);
1083        assert!(!result.timed_out);
1084    }
1085}