Skip to main content

ararajuba_tools_coding/shell/
exec.rs

1//! `execute_command` tool — run a shell command. **Requires approval.**
2
3use ararajuba_core::tools::tool::{tool, ToolDef};
4use serde_json::json;
5use tokio::process::Command;
6use tokio::time::{timeout, Duration};
7
8/// Create the `execute_command` tool.
9///
10/// Runs an arbitrary command with optional arguments, working directory, and
11/// timeout. **High-risk tool** — `needs_approval` is always set.
12pub fn execute_command_tool() -> ToolDef {
13    tool("execute_command")
14        .description(
15            "Execute a shell command. High risk — always requires approval in production.",
16        )
17        .input_schema(json!({
18            "type": "object",
19            "properties": {
20                "command":      { "type": "string", "description": "Command to run" },
21                "args":         { "type": "array", "items": { "type": "string" }, "description": "Arguments" },
22                "cwd":          { "type": "string", "description": "Working directory" },
23                "timeout_secs": { "type": "integer", "description": "Timeout in seconds (default 30)" }
24            },
25            "required": ["command"]
26        }))
27        .execute(|input| async move {
28            let command = input["command"]
29                .as_str()
30                .ok_or_else(|| "missing required field: command".to_string())?;
31
32            let args: Vec<String> = input["args"]
33                .as_array()
34                .map(|a| {
35                    a.iter()
36                        .filter_map(|v| v.as_str().map(String::from))
37                        .collect()
38                })
39                .unwrap_or_default();
40
41            let cwd = input["cwd"].as_str().unwrap_or(".");
42            let timeout_secs = input["timeout_secs"].as_u64().unwrap_or(30);
43
44            let mut cmd = Command::new(command);
45            cmd.args(&args)
46                .current_dir(cwd)
47                .stdout(std::process::Stdio::piped())
48                .stderr(std::process::Stdio::piped());
49
50            let child = cmd
51                .spawn()
52                .map_err(|e| format!("failed to spawn command: {e}"))?;
53
54            let result = timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
55                .await
56                .map_err(|_| format!("command timed out after {timeout_secs}s"))?
57                .map_err(|e| format!("command failed: {e}"))?;
58
59            let stdout = String::from_utf8_lossy(&result.stdout).to_string();
60            let stderr = String::from_utf8_lossy(&result.stderr).to_string();
61            let exit_code = result.status.code().unwrap_or(-1);
62
63            Ok(json!({
64                "stdout": stdout,
65                "stderr": stderr,
66                "exit_code": exit_code
67            }))
68        })
69        .needs_approval(|_input| true)
70        .build()
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn tool_metadata() {
79        let t = execute_command_tool();
80        assert_eq!(t.name, "execute_command");
81        assert!(t.execute.is_some());
82        assert!(t.needs_approval.is_some());
83    }
84}