Skip to main content

tiny_agent/sandbox/
host.rs

1use super::{ExecOutput, OutStream, OutputCallback, Sandbox, SandboxError};
2use async_trait::async_trait;
3use std::process::Stdio;
4use std::sync::Arc;
5use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
6use tokio::process::Command;
7
8/// 直接在宿主机上执行命令、读写真实文件系统的 sandbox。
9///
10/// 这是默认实现,行为等价于"不沙箱",不提供任何隔离。
11#[derive(Default)]
12pub struct HostSandbox;
13
14impl HostSandbox {
15    pub fn new() -> Self {
16        Self
17    }
18}
19
20#[async_trait]
21impl Sandbox for HostSandbox {
22    async fn open(self: Arc<Self>, _session_id: &str) -> Result<Arc<dyn Sandbox>, SandboxError> {
23        Ok(self)
24    }
25
26    async fn execute(
27        &self,
28        command: &str,
29        on_output: Option<OutputCallback>,
30    ) -> Result<ExecOutput, SandboxError> {
31        let mut child = Command::new("bash")
32            .arg("-c")
33            .arg(command)
34            .stdin(Stdio::null())
35            .stdout(Stdio::piped())
36            .stderr(Stdio::piped())
37            // 被 drop(取消/超时杀任务)时连带杀掉直接子进程。
38            .kill_on_drop(true)
39            .spawn()?;
40
41        let stdout = child.stdout.take().expect("stdout piped");
42        let stderr = child.stderr.take().expect("stderr piped");
43
44        // 两个流并发逐行读:每行既回调(实时)又累积(最终返回)。
45        let (out_acc, err_acc) = tokio::join!(
46            pump(stdout, OutStream::Stdout, on_output.clone()),
47            pump(stderr, OutStream::Stderr, on_output.clone()),
48        );
49
50        let status = child.wait().await?;
51        Ok(ExecOutput {
52            stdout: out_acc,
53            stderr: err_acc,
54            exit_code: status.code(),
55        })
56    }
57}
58
59/// 逐行读一个流:每行调一次回调(若有)并累积,EOF 后返回累积内容。
60async fn pump<R: AsyncRead + Unpin>(
61    reader: R,
62    stream: OutStream,
63    on_output: Option<OutputCallback>,
64) -> String {
65    let mut lines = BufReader::new(reader).lines();
66    let mut acc = String::new();
67    while let Ok(Some(line)) = lines.next_line().await {
68        if let Some(cb) = &on_output {
69            cb(stream, &line);
70        }
71        acc.push_str(&line);
72        acc.push('\n');
73    }
74    acc
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[tokio::test]
82    async fn execute_captures_stdout_and_exit_code() {
83        let sb = HostSandbox::new();
84        let out = sb.execute("echo hello", None).await.unwrap();
85        assert_eq!(out.stdout, "hello\n");
86        assert!(out.is_success());
87    }
88
89    #[tokio::test]
90    async fn execute_reports_nonzero_exit() {
91        let sb = HostSandbox::new();
92        let out = sb.execute("exit 3", None).await.unwrap();
93        assert_eq!(out.exit_code, Some(3));
94        assert!(!out.is_success());
95    }
96}