Skip to main content

sparrow/tools/
exec.rs

1use async_trait::async_trait;
2use serde_json::json;
3use std::sync::Arc;
4
5use super::{Tool, ToolCtx, ToolResult};
6use crate::event::RiskLevel;
7use crate::sandbox::{Command, Limits, Sandbox};
8
9pub struct Exec {
10    sandbox: Arc<dyn Sandbox>,
11}
12
13impl Exec {
14    pub fn new(sandbox: Arc<dyn Sandbox>) -> Self {
15        Self { sandbox }
16    }
17}
18
19#[async_trait]
20impl Tool for Exec {
21    fn name(&self) -> &str {
22        "exec"
23    }
24    fn description(&self) -> &str {
25        "Execute a shell command in the sandboxed workspace"
26    }
27    fn schema(&self) -> serde_json::Value {
28        json!({
29            "type": "object",
30            "properties": {
31                "command": { "type": "string", "description": "Shell command to execute" },
32                "timeout_ms": { "type": "integer", "description": "Timeout in milliseconds (default 120000)" }
33            },
34            "required": ["command"]
35        })
36    }
37    fn risk(&self) -> RiskLevel {
38        RiskLevel::Exec
39    }
40    async fn call(&self, args: serde_json::Value, ctx: &ToolCtx) -> anyhow::Result<ToolResult> {
41        let cmd_str = args["command"].as_str().unwrap_or("");
42        let timeout_ms = args["timeout_ms"].as_u64().unwrap_or(120_000);
43
44        let cmd = Command {
45            program: if cfg!(windows) { "cmd" } else { "sh" }.to_string(),
46            args: vec![
47                if cfg!(windows) { "/c" } else { "-c" }.to_string(),
48                cmd_str.to_string(),
49            ],
50            env: std::collections::HashMap::new(),
51            workdir: ctx.workspace_root.clone(),
52        };
53
54        let limits = Limits {
55            timeout_ms,
56            max_output_bytes: 1024 * 1024, // 1 MB
57        };
58
59        let result = self.sandbox.exec(&cmd, &limits).await?;
60
61        let output = if result.stdout.is_empty() && result.stderr.is_empty() {
62            format!("(exit: {})", result.exit_code)
63        } else if result.stderr.is_empty() {
64            result.stdout
65        } else if result.stdout.is_empty() {
66            format!("stderr:\n{}", result.stderr)
67        } else {
68            format!("stdout:\n{}\nstderr:\n{}", result.stdout, result.stderr)
69        };
70
71        Ok(ToolResult::text(output))
72    }
73}