llama_cpp_v3_agent_sdk/tools/
bash.rs1use crate::error::AgentError;
2use crate::tool::{Tool, ToolResult};
3use std::path::PathBuf;
4use std::process::Command;
5
6pub struct BashTool {
11 pub working_dir: PathBuf,
13 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 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 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}