Skip to main content

a3s_code_core/
sandbox.rs

1//! Sandbox integration for bash tool execution.
2//!
3//! When a [`BashSandbox`] is provided via
4//! [`ToolContext::with_sandbox`](crate::tools::ToolContext::with_sandbox), the
5//! `bash` built-in tool routes commands through that sandbox instead of
6//! `std::process::Command`. The workspace directory is mounted read-write
7//! at `/workspace` inside the sandbox.
8//!
9//! The concrete sandbox implementation is supplied by the host application
10//! (e.g., SafeClaw can provide an A3S Box–backed implementation after the
11//! user installs `a3s-box`). This crate defines only the trait contract.
12
13use async_trait::async_trait;
14/// Output from running a command inside a sandbox.
15pub struct SandboxOutput {
16    /// Standard output bytes decoded as UTF-8.
17    pub stdout: String,
18    /// Standard error bytes decoded as UTF-8.
19    pub stderr: String,
20    /// Process exit code (0 = success).
21    pub exit_code: i32,
22}
23
24// ============================================================================
25// BashSandbox trait
26// ============================================================================
27
28/// Abstraction over sandbox bash execution used by the `bash` built-in tool.
29///
30/// Implement this trait to provide a custom sandbox backend. The host
31/// application constructs the implementation and passes it to the session
32/// via [`ToolContext::with_sandbox`](crate::tools::ToolContext::with_sandbox).
33#[async_trait]
34pub trait BashSandbox: Send + Sync {
35    /// Execute a shell command inside the sandbox.
36    ///
37    /// * `command` — the shell command string (passed as `bash -c <command>`).
38    /// * `guest_workspace` — the guest path where the workspace is mounted
39    ///   (e.g., `"/workspace"`).
40    async fn exec_command(
41        &self,
42        command: &str,
43        guest_workspace: &str,
44    ) -> anyhow::Result<SandboxOutput>;
45
46    /// Shut down the sandbox (best-effort, infallible from caller's perspective).
47    async fn shutdown(&self);
48}
49
50// ============================================================================
51// Tests
52// ============================================================================
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use std::sync::Arc;
58
59    struct MockSandbox {
60        output: String,
61        exit_code: i32,
62    }
63
64    #[async_trait]
65    impl BashSandbox for MockSandbox {
66        async fn exec_command(
67            &self,
68            _command: &str,
69            _guest_workspace: &str,
70        ) -> anyhow::Result<SandboxOutput> {
71            Ok(SandboxOutput {
72                stdout: self.output.clone(),
73                stderr: String::new(),
74                exit_code: self.exit_code,
75            })
76        }
77
78        async fn shutdown(&self) {}
79    }
80
81    #[tokio::test]
82    async fn test_mock_sandbox_success() {
83        let sandbox = MockSandbox {
84            output: "hello sandbox\n".into(),
85            exit_code: 0,
86        };
87        let result = sandbox
88            .exec_command("echo hello sandbox", "/workspace")
89            .await
90            .unwrap();
91        assert_eq!(result.stdout, "hello sandbox\n");
92        assert_eq!(result.exit_code, 0);
93        assert!(result.stderr.is_empty());
94    }
95
96    #[tokio::test]
97    async fn test_mock_sandbox_nonzero_exit() {
98        let sandbox = MockSandbox {
99            output: String::new(),
100            exit_code: 127,
101        };
102        let result = sandbox
103            .exec_command("nonexistent_cmd", "/workspace")
104            .await
105            .unwrap();
106        assert_eq!(result.exit_code, 127);
107    }
108
109    #[tokio::test]
110    async fn test_bash_sandbox_is_arc_send_sync() {
111        let sandbox: Arc<dyn BashSandbox> = Arc::new(MockSandbox {
112            output: "ok".into(),
113            exit_code: 0,
114        });
115        let result = sandbox.exec_command("true", "/workspace").await.unwrap();
116        assert_eq!(result.exit_code, 0);
117    }
118}