Skip to main content

a3s_code_core/
sandbox.rs

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