1use 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
15const MAX_STDOUT_BYTES: usize = 10 * 1024 * 1024; const MAX_FILE_DOWNLOAD_BYTES: usize = 50 * 1024 * 1024; #[async_trait]
22pub trait SandboxOrchestrator: Send + Sync {
23 async fn create_sandbox(&self, request: SandboxRequest) -> Result<SandboxInfo, SandboxError>;
25
26 async fn start_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError>;
28
29 async fn stop_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError>;
31
32 async fn destroy_sandbox(&self, sandbox_id: SandboxId) -> Result<(), SandboxError>;
34
35 async fn get_sandbox_info(&self, sandbox_id: SandboxId) -> Result<SandboxInfo, SandboxError>;
37
38 async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>, SandboxError>;
40
41 async fn execute_command(
43 &self,
44 sandbox_id: SandboxId,
45 command: SandboxCommand,
46 ) -> Result<CommandResult, SandboxError>;
47
48 async fn upload_files(
50 &self,
51 sandbox_id: SandboxId,
52 files: Vec<FileUpload>,
53 ) -> Result<(), SandboxError>;
54
55 async fn download_files(
57 &self,
58 sandbox_id: SandboxId,
59 paths: Vec<String>,
60 ) -> Result<Vec<FileDownload>, SandboxError>;
61
62 async fn get_resource_usage(
64 &self,
65 sandbox_id: SandboxId,
66 ) -> Result<SandboxResourceUsage, SandboxError>;
67
68 async fn update_sandbox(
70 &self,
71 sandbox_id: SandboxId,
72 config: SandboxConfig,
73 ) -> Result<(), SandboxError>;
74
75 async fn get_logs(
77 &self,
78 sandbox_id: SandboxId,
79 options: LogOptions,
80 ) -> Result<Vec<LogEntry>, SandboxError>;
81
82 async fn create_snapshot(
84 &self,
85 sandbox_id: SandboxId,
86 name: String,
87 ) -> Result<SnapshotId, SandboxError>;
88
89 async fn restore_snapshot(
91 &self,
92 sandbox_id: SandboxId,
93 snapshot_id: SnapshotId,
94 ) -> Result<(), SandboxError>;
95
96 async fn delete_snapshot(&self, snapshot_id: SnapshotId) -> Result<(), SandboxError>;
98
99 async fn execute_code(
101 &self,
102 tier: SandboxTier,
103 code: &str,
104 env: HashMap<String, String>,
105 ) -> Result<ExecutionResult, SandboxError>;
106
107 async fn register_sandbox_runner(
109 &self,
110 tier: SandboxTier,
111 runner: Arc<dyn SandboxRunner>,
112 ) -> Result<(), SandboxError>;
113}
114
115#[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#[derive(Debug, Clone, Serialize, Deserialize)]
130pub enum SandboxType {
131 Docker { image: String, tag: String },
133 #[cfg(feature = "enterprise")]
135 GVisor { runtime: String, platform: String },
136 #[cfg(feature = "enterprise")]
138 Firecracker {
139 kernel_image: String,
140 rootfs_image: String,
141 },
142 Process {
144 executable: String,
145 working_dir: PathBuf,
146 },
147 Custom {
149 provider: String,
150 config: HashMap<String, String>,
151 },
152}
153
154#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
187pub enum RestartPolicy {
188 Never,
189 Always,
190 OnFailure { max_retries: u32 },
191 UnlessStopped,
192}
193
194#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
237pub enum Protocol {
238 TCP,
239 UDP,
240 SCTP,
241}
242
243#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
264pub enum MountType {
265 Bind,
266 Volume,
267 Tmpfs,
268}
269
270#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
442pub enum LogLevel {
443 Trace,
444 Debug,
445 Info,
446 Warning,
447 Error,
448 Fatal,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub enum LogSource {
454 Stdout,
455 Stderr,
456 System,
457 Application,
458}
459
460pub type SandboxId = uuid::Uuid;
462
463pub type SnapshotId = uuid::Uuid;
465
466pub 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#[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, peak: 128 * 1024 * 1024, limit: 512 * 1024 * 1024, cache: 16 * 1024 * 1024, swap: 0,
549 percent: 12.5,
550 },
551 disk_usage: DiskUsage {
552 read_bytes: 1024 * 1024, write_bytes: 512 * 1024, read_ops: 100,
555 write_ops: 50,
556 total_space: 10 * 1024 * 1024 * 1024, used_space: 1024 * 1024 * 1024, },
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 let stdout = format!("Mock output for command: {:?}", command.command);
664 let stderr = String::new();
665
666 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 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); 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 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 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 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 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}