ararajuba_tools_coding/shell/
exec.rs1use ararajuba_core::tools::tool::{tool, ToolDef};
4use serde_json::json;
5use tokio::process::Command;
6use tokio::time::{timeout, Duration};
7
8pub 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}