use crate::error::AgentError;
use crate::tool::{Tool, ToolResult};
use std::path::PathBuf;
use std::process::Command;
pub struct BashTool {
pub working_dir: PathBuf,
pub max_output_chars: usize,
}
impl BashTool {
pub fn new() -> Self {
Self {
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
max_output_chars: 8192,
}
}
pub fn with_working_dir(mut self, dir: PathBuf) -> Self {
self.working_dir = dir;
self
}
}
impl Default for BashTool {
fn default() -> Self {
Self::new()
}
}
impl Tool for BashTool {
fn name(&self) -> &str {
"bash"
}
fn description(&self) -> &str {
"Execute a shell command and return its output (stdout + stderr). \
Output is truncated to keep the tail on long output."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
}
},
"required": ["command"]
})
}
fn execute(&self, args: &serde_json::Value) -> Result<ToolResult, AgentError> {
let command = args["command"]
.as_str()
.ok_or_else(|| AgentError::Tool {
tool: "bash".to_string(),
message: "Missing 'command' argument".to_string(),
})?;
let output = if cfg!(windows) {
Command::new("cmd")
.args(["/C", command])
.current_dir(&self.working_dir)
.output()
} else {
Command::new("sh")
.args(["-c", command])
.current_dir(&self.working_dir)
.output()
};
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut combined = String::new();
if !stdout.is_empty() {
combined.push_str(&stdout);
}
if !stderr.is_empty() {
if !combined.is_empty() {
combined.push('\n');
}
combined.push_str("[stderr]\n");
combined.push_str(&stderr);
}
if combined.len() > self.max_output_chars {
let skip = combined.len() - self.max_output_chars;
combined = format!(
"[...truncated {} chars...]\n{}",
skip,
&combined[skip..]
);
}
let exit_code = output.status.code().unwrap_or(-1);
if exit_code != 0 {
combined.push_str(&format!("\n[exit code: {}]", exit_code));
}
Ok(if output.status.success() {
ToolResult::ok(combined)
} else {
ToolResult::err(combined)
})
}
Err(e) => Ok(ToolResult::err(format!("Failed to execute command: {}", e))),
}
}
fn requires_permission(&self) -> bool {
true
}
fn is_dangerous(&self, args: &serde_json::Value) -> bool {
if let Some(cmd) = args["command"].as_str() {
let cmd_lower = cmd.to_lowercase();
cmd_lower.contains("rm -rf")
|| cmd_lower.contains("sudo")
|| cmd_lower.contains("curl") && cmd_lower.contains("| bash")
|| cmd_lower.contains("curl") && cmd_lower.contains("| sh")
|| cmd_lower.contains("format")
|| cmd_lower.contains("mkfs")
|| cmd_lower.contains("dd if=")
} else {
false
}
}
}