use super::traits::Tool;
use crate::config::constants::tools;
use anyhow::{Context, Result};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::{path::PathBuf, process::Stdio, time::Duration};
use tokio::{process::Command, time::timeout};
#[derive(Clone)]
pub struct BashTool {
workspace_root: PathBuf,
}
impl BashTool {
pub fn new(workspace_root: PathBuf) -> Self {
Self { workspace_root }
}
async fn execute_pty_command(
&self,
command: &str,
args: Vec<String>,
timeout_secs: Option<u64>,
) -> Result<Value> {
let full_command_parts = std::iter::once(command.to_string())
.chain(args.clone())
.collect::<Vec<String>>();
self.validate_command(&full_command_parts)?;
let full_command = if args.is_empty() {
command.to_string()
} else {
format!("{} {}", command, args.join(" "))
};
let work_dir = self.workspace_root.clone();
let mut cmd = Command::new(command);
if !args.is_empty() {
cmd.args(&args);
}
cmd.current_dir(&work_dir);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let duration = Duration::from_secs(timeout_secs.unwrap_or(30));
let output = timeout(duration, cmd.output())
.await
.with_context(|| {
format!(
"command '{}' timed out after {}s",
full_command,
duration.as_secs()
)
})?
.with_context(|| format!("Failed to execute command: {}", full_command))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(json!({
"success": output.status.success(),
"exit_code": output.status.code().unwrap_or_default(),
"stdout": stdout,
"stderr": stderr,
"mode": "terminal",
"pty_enabled": false,
"command": full_command,
"working_directory": work_dir.display().to_string()
}))
}
fn validate_command(&self, command_parts: &[String]) -> Result<()> {
if command_parts.is_empty() {
return Err(anyhow::anyhow!("Command cannot be empty"));
}
let program = &command_parts[0];
let dangerous_commands = [
"rm",
"rmdir",
"del",
"format",
"fdisk",
"mkfs",
"dd",
"shred",
"wipe",
"srm",
"unlink",
"chmod",
"chown",
"passwd",
"usermod",
"userdel",
"systemctl",
"service",
"kill",
"killall",
"pkill",
"reboot",
"shutdown",
"halt",
"poweroff",
"sudo",
"su",
"doas",
"runas",
"curl",
"wget",
"ftp",
"scp",
"rsync", "ssh",
"telnet",
"nc",
"ncat",
"socat", "mount",
"umount",
"fsck",
"tune2fs", "iptables",
"ufw",
"firewalld", "crontab",
"at", "docker",
"podman",
"kubectl", ];
if dangerous_commands.contains(&program.as_str()) {
return Err(anyhow::anyhow!(
"Dangerous command not allowed: '{}'. This command could potentially harm your system. \
Use file operation tools instead for safe file management.",
program
));
}
let full_command = command_parts.join(" ");
if full_command.contains("rm -rf")
|| full_command.contains("rm -r")
&& (full_command.contains(" /") || full_command.contains(" ~"))
|| full_command.contains("rmdir")
&& (full_command.contains(" /") || full_command.contains(" ~"))
{
return Err(anyhow::anyhow!(
"Potentially dangerous recursive delete operation detected. \
Use file operation tools for safe file management."
));
}
if full_command.contains("sudo ")
|| full_command.contains("su ")
|| full_command.contains("doas ")
|| full_command.contains("runas ")
{
return Err(anyhow::anyhow!(
"Privilege escalation commands are not allowed. \
All operations run with current user privileges."
));
}
if (full_command.contains("curl ") || full_command.contains("wget "))
&& (full_command.contains("http://")
|| full_command.contains("https://")
|| full_command.contains("ftp://"))
{
return Err(anyhow::anyhow!(
"Network download commands are restricted. \
Use local file operations only."
));
}
if full_command.contains(" > /etc/")
|| full_command.contains(" >> /etc/")
|| full_command.contains(" > /usr/")
|| full_command.contains(" >> /usr/")
|| full_command.contains(" > /var/")
|| full_command.contains(" >> /var/")
{
return Err(anyhow::anyhow!(
"System configuration file modifications are not allowed. \
Use user-specific configuration files only."
));
}
let sensitive_paths = [
"/etc/", "/usr/", "/var/", "/root/", "/boot/", "/sys/", "/proc/",
];
for path in &sensitive_paths {
if full_command.contains(path)
&& (full_command.contains("rm ")
|| full_command.contains("mv ")
|| full_command.contains("cp ")
|| full_command.contains("chmod ")
|| full_command.contains("chown "))
{
return Err(anyhow::anyhow!(
"Operations on system directories '{}' are not allowed. \
Work within your project workspace only.",
path.trim_end_matches('/')
));
}
}
let allowed_commands = [
"ls", "pwd", "cat", "head", "tail", "grep", "find", "wc", "sort", "uniq", "cut", "awk",
"sed", "echo", "printf", "seq", "basename", "dirname", "date", "cal", "bc", "expr",
"test", "[", "]", "true", "false", "sleep", "which", "type", "file", "stat", "du",
"df", "ps", "top", "htop", "tree", "less", "more", "tac", "rev", "tr", "fold", "paste",
"join", "comm", "diff", "patch", "gzip", "gunzip", "bzip2", "bunzip2", "xz", "unxz",
"tar", "zip", "unzip", "gzip", "bzip2", "git", "hg",
"svn", "make", "cmake", "ninja", "cargo", "npm", "yarn", "pnpm", "python", "python3", "node", "ruby", "perl", "php", "java", "javac", "scala", "kotlin",
"go", "rustc", "gcc", "g++", "clang", "clang++", ];
if !allowed_commands.contains(&program.as_str()) {
return Err(anyhow::anyhow!(
"Command '{}' is not in the allowed commands list. \
Only safe development and analysis commands are permitted. \
Use specialized tools for file operations, searches, and builds.",
program
));
}
Ok(())
}
async fn execute_ls(&self, args: Value) -> Result<Value> {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
let show_hidden = args
.get("show_hidden")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut cmd_args = vec![path.to_string()];
if show_hidden {
cmd_args.insert(0, "-la".to_string());
} else {
cmd_args.insert(0, "-l".to_string());
}
self.execute_pty_command("ls", cmd_args, Some(10)).await
}
async fn execute_pwd(&self) -> Result<Value> {
self.execute_pty_command("pwd", vec![], Some(5)).await
}
async fn execute_grep(&self, args: Value) -> Result<Value> {
let pattern = args
.get("pattern")
.and_then(|v| v.as_str())
.context("pattern is required for grep")?;
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
let recursive = args
.get("recursive")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut cmd_args = vec![pattern.to_string(), path.to_string()];
if recursive {
cmd_args.insert(0, "-r".to_string());
}
self.execute_pty_command("grep", cmd_args, Some(30)).await
}
async fn execute_find(&self, args: Value) -> Result<Value> {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
let name_pattern = args.get("name_pattern").and_then(|v| v.as_str());
let type_filter = args.get("type_filter").and_then(|v| v.as_str());
let mut cmd_args = vec![path.to_string()];
if let Some(pattern) = name_pattern {
cmd_args.push("-name".to_string());
cmd_args.push(pattern.to_string());
}
if let Some(filter) = type_filter {
cmd_args.push("-type".to_string());
cmd_args.push(filter.to_string());
}
self.execute_pty_command("find", cmd_args, Some(30)).await
}
async fn execute_cat(&self, args: Value) -> Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.context("path is required for cat")?;
let start_line = args.get("start_line").and_then(|v| v.as_u64());
let end_line = args.get("end_line").and_then(|v| v.as_u64());
if let (Some(start), Some(end)) = (start_line, end_line) {
let sed_cmd = format!("sed -n '{}','{}'p {}", start, end, path);
return self
.execute_pty_command("sh", vec!["-c".to_string(), sed_cmd], Some(10))
.await;
}
let cmd_args = vec![path.to_string()];
self.execute_pty_command("cat", cmd_args, Some(10)).await
}
async fn execute_head(&self, args: Value) -> Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.context("path is required for head")?;
let lines = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(10);
let cmd_args = vec!["-n".to_string(), lines.to_string(), path.to_string()];
self.execute_pty_command("head", cmd_args, Some(10)).await
}
async fn execute_tail(&self, args: Value) -> Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.context("path is required for tail")?;
let lines = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(10);
let cmd_args = vec!["-n".to_string(), lines.to_string(), path.to_string()];
self.execute_pty_command("tail", cmd_args, Some(10)).await
}
async fn execute_mkdir(&self, args: Value) -> Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.context("path is required for mkdir")?;
let parents = args
.get("parents")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut cmd_args = vec![path.to_string()];
if parents {
cmd_args.insert(0, "-p".to_string());
}
self.execute_pty_command("mkdir", cmd_args, Some(10)).await
}
async fn execute_rm(&self, args: Value) -> Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.context("path is required for rm")?;
let recursive = args
.get("recursive")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
let mut cmd_args = vec![];
if recursive {
cmd_args.push("-r".to_string());
}
if force {
cmd_args.push("-f".to_string());
}
cmd_args.push(path.to_string());
self.execute_pty_command("rm", cmd_args, Some(10)).await
}
async fn execute_cp(&self, args: Value) -> Result<Value> {
let source = args
.get("source")
.and_then(|v| v.as_str())
.context("source is required for cp")?;
let dest = args
.get("dest")
.and_then(|v| v.as_str())
.context("dest is required for cp")?;
let recursive = args
.get("recursive")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut cmd_args = vec![];
if recursive {
cmd_args.push("-r".to_string());
}
cmd_args.push(source.to_string());
cmd_args.push(dest.to_string());
self.execute_pty_command("cp", cmd_args, Some(30)).await
}
async fn execute_mv(&self, args: Value) -> Result<Value> {
let source = args
.get("source")
.and_then(|v| v.as_str())
.context("source is required for mv")?;
let dest = args
.get("dest")
.and_then(|v| v.as_str())
.context("dest is required for mv")?;
let cmd_args = vec![source.to_string(), dest.to_string()];
self.execute_pty_command("mv", cmd_args, Some(10)).await
}
async fn execute_stat(&self, args: Value) -> Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.context("path is required for stat")?;
let cmd_args = vec!["-la".to_string(), path.to_string()];
self.execute_pty_command("ls", cmd_args, Some(10)).await
}
async fn execute_run(&self, args: Value) -> Result<Value> {
let command = args
.get("command")
.and_then(|v| v.as_str())
.context("command is required for run")?;
let cmd_args = args
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect::<Vec<String>>()
})
.unwrap_or_default();
self.execute_pty_command(command, cmd_args, Some(30)).await
}
}
#[async_trait]
impl Tool for BashTool {
async fn execute(&self, args: Value) -> Result<Value> {
let command = args
.get("bash_command")
.and_then(|v| v.as_str())
.unwrap_or("ls");
match command {
"ls" => self.execute_ls(args).await,
"pwd" => self.execute_pwd().await,
"grep" => self.execute_grep(args).await,
"find" => self.execute_find(args).await,
"cat" => self.execute_cat(args).await,
"head" => self.execute_head(args).await,
"tail" => self.execute_tail(args).await,
"mkdir" => self.execute_mkdir(args).await,
"rm" => self.execute_rm(args).await,
"cp" => self.execute_cp(args).await,
"mv" => self.execute_mv(args).await,
"stat" => self.execute_stat(args).await,
"run" => self.execute_run(args).await,
_ => Err(anyhow::anyhow!("Unknown bash command: {}", command)),
}
}
fn name(&self) -> &'static str {
tools::BASH
}
fn description(&self) -> &'static str {
"Bash-like commands with security validation: ls, pwd, grep, find, cat, head, tail, mkdir, rm, cp, mv, stat, run. \
Dangerous commands (rm, sudo, network operations, system modifications) are blocked for safety."
}
}