Skip to main content

llama_cpp_v3_agent_sdk/tools/
bash.rs

1use crate::error::AgentError;
2use crate::tool::{Tool, ToolResult};
3use std::path::PathBuf;
4use std::process::Command;
5
6/// Execute shell commands.
7///
8/// Output is captured and truncated to a configurable maximum length,
9/// keeping the tail (so error messages at the end are preserved).
10pub struct BashTool {
11    /// Working directory for commands. Defaults to current directory.
12    pub working_dir: PathBuf,
13    /// Maximum output length in characters.
14    pub max_output_chars: usize,
15}
16
17impl BashTool {
18    pub fn new() -> Self {
19        Self {
20            working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
21            max_output_chars: 8192,
22        }
23    }
24
25    pub fn with_working_dir(mut self, dir: PathBuf) -> Self {
26        self.working_dir = dir;
27        self
28    }
29}
30
31impl Default for BashTool {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Tool for BashTool {
38    fn name(&self) -> &str {
39        "bash"
40    }
41
42    fn description(&self) -> &str {
43        "Execute a shell command and return its output (stdout + stderr). \
44         Output is truncated to keep the tail on long output."
45    }
46
47    fn parameters_schema(&self) -> serde_json::Value {
48        serde_json::json!({
49            "type": "object",
50            "properties": {
51                "command": {
52                    "type": "string",
53                    "description": "The shell command to execute"
54                }
55            },
56            "required": ["command"]
57        })
58    }
59
60    fn execute(&self, args: &serde_json::Value) -> Result<ToolResult, AgentError> {
61        let command = args["command"]
62            .as_str()
63            .ok_or_else(|| AgentError::Tool {
64                tool: "bash".to_string(),
65                message: "Missing 'command' argument".to_string(),
66            })?;
67
68        let output = if cfg!(windows) {
69            Command::new("cmd")
70                .args(["/C", command])
71                .current_dir(&self.working_dir)
72                .output()
73        } else {
74            Command::new("sh")
75                .args(["-c", command])
76                .current_dir(&self.working_dir)
77                .output()
78        };
79
80        match output {
81            Ok(output) => {
82                let stdout = String::from_utf8_lossy(&output.stdout);
83                let stderr = String::from_utf8_lossy(&output.stderr);
84                let mut combined = String::new();
85
86                if !stdout.is_empty() {
87                    combined.push_str(&stdout);
88                }
89                if !stderr.is_empty() {
90                    if !combined.is_empty() {
91                        combined.push('\n');
92                    }
93                    combined.push_str("[stderr]\n");
94                    combined.push_str(&stderr);
95                }
96
97                // Truncate keeping the tail
98                if combined.len() > self.max_output_chars {
99                    let skip = combined.len() - self.max_output_chars;
100                    combined = format!(
101                        "[...truncated {} chars...]\n{}",
102                        skip,
103                        &combined[skip..]
104                    );
105                }
106
107                let exit_code = output.status.code().unwrap_or(-1);
108                if exit_code != 0 {
109                    combined.push_str(&format!("\n[exit code: {}]", exit_code));
110                }
111
112                Ok(if output.status.success() {
113                    ToolResult::ok(combined)
114                } else {
115                    ToolResult::err(combined)
116                })
117            }
118            Err(e) => Ok(ToolResult::err(format!("Failed to execute command: {}", e))),
119        }
120    }
121
122    fn requires_permission(&self) -> bool {
123        true
124    }
125
126    fn is_dangerous(&self, args: &serde_json::Value) -> bool {
127        if let Some(cmd) = args["command"].as_str() {
128            let cmd_lower = cmd.to_lowercase();
129            // Common dangerous patterns
130            cmd_lower.contains("rm -rf")
131                || cmd_lower.contains("sudo")
132                || cmd_lower.contains("curl") && cmd_lower.contains("| bash")
133                || cmd_lower.contains("curl") && cmd_lower.contains("| sh")
134                || cmd_lower.contains("format")
135                || cmd_lower.contains("mkfs")
136                || cmd_lower.contains("dd if=")
137        } else {
138            false
139        }
140    }
141}