tiny_agent/sandbox/
host.rs1use 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#[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 .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 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
59async 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}