Skip to main content

agent_sandbox/
lib.rs

1pub mod config;
2pub mod error;
3pub mod exec;
4pub mod fs;
5pub mod runtime;
6pub mod toolbox;
7
8use std::sync::Arc;
9
10use tokio::sync::Mutex;
11
12use crate::config::SandboxConfig;
13use crate::error::{Result, SandboxError};
14use crate::fs::overlay::{FsChange, FsOverlay};
15use crate::runtime::{ExecResult, WasiRuntime};
16
17/// A sandboxed execution environment backed by WASM (Wasmtime + WASI).
18pub struct Sandbox {
19    runtime: WasiRuntime,
20    overlay: Arc<Mutex<Option<FsOverlay>>>,
21    config: SandboxConfig,
22    destroyed: Arc<std::sync::atomic::AtomicBool>,
23}
24
25impl Sandbox {
26    /// Create a new sandbox with the given configuration.
27    pub fn new(config: SandboxConfig) -> Result<Self> {
28        let overlay = FsOverlay::new(&config.work_dir)?;
29        let runtime = WasiRuntime::new(config.clone())?;
30
31        Ok(Self {
32            runtime,
33            overlay: Arc::new(Mutex::new(Some(overlay))),
34            config,
35            destroyed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
36        })
37    }
38
39    /// Execute a command inside the sandbox.
40    pub async fn exec(&self, command: &str, args: &[String]) -> Result<ExecResult> {
41        self.check_destroyed()?;
42
43        if !toolbox::is_available(command) {
44            return Err(SandboxError::CommandNotFound(command.to_string()));
45        }
46
47        self.runtime.exec(command, args).await
48    }
49
50    /// Read a file from the sandbox's work directory.
51    pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
52        self.check_destroyed()?;
53
54        let full_path = fs::validate_path(&self.config.work_dir, path)?;
55        let content = tokio::fs::read(&full_path).await?;
56        Ok(content)
57    }
58
59    /// Write a file to the sandbox's work directory.
60    pub async fn write_file(&self, path: &str, contents: &[u8]) -> Result<()> {
61        self.check_destroyed()?;
62
63        let full_path = fs::validate_path(&self.config.work_dir, path)?;
64
65        // Ensure parent directory exists
66        if let Some(parent) = full_path.parent() {
67            tokio::fs::create_dir_all(parent).await?;
68        }
69
70        tokio::fs::write(&full_path, contents).await?;
71        Ok(())
72    }
73
74    /// List entries in a directory within the sandbox.
75    pub async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
76        self.check_destroyed()?;
77
78        let full_path = fs::validate_path(&self.config.work_dir, path)?;
79        let mut entries = Vec::new();
80
81        let mut rd = tokio::fs::read_dir(&full_path).await?;
82        while let Some(entry) = rd.next_entry().await? {
83            let metadata = entry.metadata().await?;
84            entries.push(DirEntry {
85                name: entry.file_name().to_string_lossy().to_string(),
86                is_dir: metadata.is_dir(),
87                is_file: metadata.is_file(),
88                size: metadata.len(),
89            });
90        }
91
92        entries.sort_by(|a, b| a.name.cmp(&b.name));
93        Ok(entries)
94    }
95
96    /// Get filesystem changes since the sandbox was created.
97    pub async fn diff(&self) -> Result<Vec<FsChange>> {
98        self.check_destroyed()?;
99
100        let overlay = self.overlay.lock().await;
101        match overlay.as_ref() {
102            Some(o) => o.diff(),
103            None => Err(SandboxError::Destroyed),
104        }
105    }
106
107    /// Destroy the sandbox, cleaning up temporary resources.
108    pub async fn destroy(&self) -> Result<()> {
109        self.destroyed
110            .store(true, std::sync::atomic::Ordering::SeqCst);
111        let mut overlay = self.overlay.lock().await;
112        *overlay = None;
113        Ok(())
114    }
115
116    fn check_destroyed(&self) -> Result<()> {
117        if self.destroyed.load(std::sync::atomic::Ordering::SeqCst) {
118            Err(SandboxError::Destroyed)
119        } else {
120            Ok(())
121        }
122    }
123}
124
125/// A directory entry returned by `list_dir`.
126#[derive(Debug, Clone)]
127pub struct DirEntry {
128    pub name: String,
129    pub is_dir: bool,
130    pub is_file: bool,
131    pub size: u64,
132}