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    /// Execute JavaScript code inside the sandbox using the built-in JS engine.
51    pub async fn exec_js(&self, code: &str) -> Result<ExecResult> {
52        self.exec("node", &["-e".to_string(), code.to_string()])
53            .await
54    }
55
56    /// Read a file from the sandbox's work directory.
57    pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
58        self.check_destroyed()?;
59
60        let full_path = fs::validate_path(&self.config.work_dir, path)?;
61        let content = tokio::fs::read(&full_path).await?;
62        Ok(content)
63    }
64
65    /// Write a file to the sandbox's work directory.
66    pub async fn write_file(&self, path: &str, contents: &[u8]) -> Result<()> {
67        self.check_destroyed()?;
68
69        let full_path = fs::validate_path(&self.config.work_dir, path)?;
70
71        // Ensure parent directory exists
72        if let Some(parent) = full_path.parent() {
73            tokio::fs::create_dir_all(parent).await?;
74        }
75
76        tokio::fs::write(&full_path, contents).await?;
77        Ok(())
78    }
79
80    /// List entries in a directory within the sandbox.
81    pub async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
82        self.check_destroyed()?;
83
84        let full_path = fs::validate_path(&self.config.work_dir, path)?;
85        let mut entries = Vec::new();
86
87        let mut rd = tokio::fs::read_dir(&full_path).await?;
88        while let Some(entry) = rd.next_entry().await? {
89            let metadata = entry.metadata().await?;
90            entries.push(DirEntry {
91                name: entry.file_name().to_string_lossy().to_string(),
92                is_dir: metadata.is_dir(),
93                is_file: metadata.is_file(),
94                size: metadata.len(),
95            });
96        }
97
98        entries.sort_by(|a, b| a.name.cmp(&b.name));
99        Ok(entries)
100    }
101
102    /// Get filesystem changes since the sandbox was created.
103    pub async fn diff(&self) -> Result<Vec<FsChange>> {
104        self.check_destroyed()?;
105
106        let overlay = self.overlay.lock().await;
107        match overlay.as_ref() {
108            Some(o) => o.diff(),
109            None => Err(SandboxError::Destroyed),
110        }
111    }
112
113    /// Destroy the sandbox, cleaning up temporary resources.
114    pub async fn destroy(&self) -> Result<()> {
115        self.destroyed
116            .store(true, std::sync::atomic::Ordering::SeqCst);
117        let mut overlay = self.overlay.lock().await;
118        *overlay = None;
119        Ok(())
120    }
121
122    fn check_destroyed(&self) -> Result<()> {
123        if self.destroyed.load(std::sync::atomic::Ordering::SeqCst) {
124            Err(SandboxError::Destroyed)
125        } else {
126            Ok(())
127        }
128    }
129}
130
131/// A directory entry returned by `list_dir`.
132#[derive(Debug, Clone)]
133pub struct DirEntry {
134    pub name: String,
135    pub is_dir: bool,
136    pub is_file: bool,
137    pub size: u64,
138}