Skip to main content

agentic_sandbox/
process.rs

1//! Process-based sandbox for local development.
2//!
3//! **Warning**: This sandbox provides NO isolation and should only be used
4//! for development with trusted code.
5
6use async_trait::async_trait;
7use std::process::Stdio;
8use tokio::process::Command;
9use tracing::{debug, instrument, warn};
10
11use crate::{
12    error::SandboxError,
13    traits::{ExecutionResult, Sandbox},
14};
15
16/// Process-based sandbox for development.
17///
18/// Executes code directly in a subprocess. Provides NO security isolation.
19#[derive(Clone)]
20pub struct ProcessSandbox {
21    timeout_ms: u64,
22    shell: String,
23}
24
25impl ProcessSandbox {
26    /// Create a new process sandbox.
27    #[must_use]
28    pub fn new() -> Self {
29        Self {
30            timeout_ms: 30000,
31            shell: if cfg!(windows) {
32                "cmd".to_string()
33            } else {
34                "sh".to_string()
35            },
36        }
37    }
38
39    /// Set execution timeout.
40    #[must_use]
41    pub const fn with_timeout(mut self, timeout_ms: u64) -> Self {
42        self.timeout_ms = timeout_ms;
43        self
44    }
45
46    /// Set shell to use.
47    #[must_use]
48    pub fn with_shell(mut self, shell: impl Into<String>) -> Self {
49        self.shell = shell.into();
50        self
51    }
52}
53
54impl Default for ProcessSandbox {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60#[async_trait]
61impl Sandbox for ProcessSandbox {
62    #[instrument(skip(self, code), fields(sandbox = "process"))]
63    async fn execute(&self, code: &str) -> Result<ExecutionResult, SandboxError> {
64        warn!("ProcessSandbox provides NO security isolation!");
65        debug!("Executing code: {}...", &code[..code.len().min(100)]);
66
67        let start = std::time::Instant::now();
68
69        let shell_arg = if cfg!(windows) { "/C" } else { "-c" };
70
71        let output = tokio::time::timeout(
72            std::time::Duration::from_millis(self.timeout_ms),
73            Command::new(&self.shell)
74                .arg(shell_arg)
75                .arg(code)
76                .stdout(Stdio::piped())
77                .stderr(Stdio::piped())
78                .output(),
79        )
80        .await
81        .map_err(|_| SandboxError::Timeout)?
82        .map_err(SandboxError::IoError)?;
83
84        #[allow(clippy::cast_possible_truncation)]
85        let execution_time_ms = start.elapsed().as_millis() as u64;
86
87        Ok(ExecutionResult {
88            exit_code: output.status.code().unwrap_or(-1),
89            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
90            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
91            execution_time_ms,
92            artifacts: vec![],
93        })
94    }
95
96    async fn is_ready(&self) -> Result<bool, SandboxError> {
97        Ok(true) // Process sandbox is always ready
98    }
99
100    async fn stop(&self) -> Result<(), SandboxError> {
101        Ok(()) // Nothing to clean up
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[tokio::test]
110    async fn test_simple_command() {
111        let sandbox = ProcessSandbox::new();
112        let result = sandbox.execute("echo hello").await.unwrap();
113
114        assert!(result.is_success());
115        assert!(result.stdout.contains("hello"));
116    }
117
118    #[tokio::test]
119    async fn test_exit_code() {
120        let sandbox = ProcessSandbox::new();
121        let result = sandbox.execute("exit 42").await.unwrap();
122
123        assert_eq!(result.exit_code, 42);
124    }
125}