Skip to main content

pi_agent/tools/
bash.rs

1use async_trait::async_trait;
2use serde_json::{json, Value};
3use tokio::process::Command;
4use tokio::time::{timeout, Duration};
5
6use crate::types::{AgentTool, AgentToolResult};
7
8pub struct BashTool;
9
10#[async_trait]
11impl AgentTool for BashTool {
12    fn name(&self) -> &str {
13        "bash"
14    }
15    fn requires_permission(&self) -> bool {
16        true
17    }
18    fn description(&self) -> &str {
19        "Run a shell command via `bash -lc <cmd>`. Returns combined stdout/stderr and exit code."
20    }
21    fn parameters(&self) -> Value {
22        json!({
23            "type": "object",
24            "properties": {
25                "command": {"type": "string"},
26                "timeout_ms": {"type": "integer", "default": 120000}
27            },
28            "required": ["command"]
29        })
30    }
31    async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
32        let cmd = args
33            .get("command")
34            .and_then(|v| v.as_str())
35            .ok_or("missing 'command'")?;
36        let timeout_ms = args
37            .get("timeout_ms")
38            .and_then(|v| v.as_u64())
39            .unwrap_or(120_000);
40
41        let fut = Command::new("bash").arg("-lc").arg(cmd).output();
42        let output = match timeout(Duration::from_millis(timeout_ms), fut).await {
43            Ok(Ok(o)) => o,
44            Ok(Err(e)) => return Err(format!("spawn: {e}")),
45            Err(_) => return Err(format!("command timed out after {timeout_ms}ms")),
46        };
47        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
48        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
49        let code = output.status.code().unwrap_or(-1);
50        let mut combined = String::new();
51        if !stdout.is_empty() {
52            combined.push_str(&stdout);
53        }
54        if !stderr.is_empty() {
55            if !combined.is_empty() && !combined.ends_with('\n') {
56                combined.push('\n');
57            }
58            combined.push_str("[stderr]\n");
59            combined.push_str(&stderr);
60        }
61        combined.push_str(&format!("\n[exit {code}]"));
62        Ok(AgentToolResult::text(combined))
63    }
64}