1use super::traits::Tool;
7use crate::config::constants::tools;
8use anyhow::{Context, Result};
9use async_trait::async_trait;
10use serde_json::{Value, json};
11use std::{path::PathBuf, process::Stdio, time::Duration};
12use tokio::{process::Command, time::timeout};
13
14#[derive(Clone)]
16pub struct BashTool {
17 workspace_root: PathBuf,
18}
19
20impl BashTool {
21 pub fn new(workspace_root: PathBuf) -> Self {
23 Self { workspace_root }
24 }
25
26 async fn execute_pty_command(
28 &self,
29 command: &str,
30 args: Vec<String>,
31 timeout_secs: Option<u64>,
32 ) -> Result<Value> {
33 let full_command_parts = std::iter::once(command.to_string())
34 .chain(args.clone())
35 .collect::<Vec<String>>();
36 self.validate_command(&full_command_parts)?;
37
38 let full_command = if args.is_empty() {
39 command.to_string()
40 } else {
41 format!("{} {}", command, args.join(" "))
42 };
43
44 let work_dir = self.workspace_root.clone();
45 let mut cmd = Command::new(command);
46 if !args.is_empty() {
47 cmd.args(&args);
48 }
49 cmd.current_dir(&work_dir);
50 cmd.stdout(Stdio::piped());
51 cmd.stderr(Stdio::piped());
52
53 let duration = Duration::from_secs(timeout_secs.unwrap_or(30));
54 let output = timeout(duration, cmd.output())
55 .await
56 .with_context(|| {
57 format!(
58 "command '{}' timed out after {}s",
59 full_command,
60 duration.as_secs()
61 )
62 })?
63 .with_context(|| format!("Failed to execute command: {}", full_command))?;
64 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
65 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
66
67 Ok(json!({
68 "success": output.status.success(),
69 "exit_code": output.status.code().unwrap_or_default(),
70 "stdout": stdout,
71 "stderr": stderr,
72 "mode": "terminal",
73 "pty_enabled": false,
74 "command": full_command,
75 "working_directory": work_dir.display().to_string()
76 }))
77 }
78
79 fn validate_command(&self, command_parts: &[String]) -> Result<()> {
81 if command_parts.is_empty() {
82 return Err(anyhow::anyhow!("Command cannot be empty"));
83 }
84
85 let program = &command_parts[0];
86
87 let dangerous_commands = [
89 "rm",
90 "rmdir",
91 "del",
92 "format",
93 "fdisk",
94 "mkfs",
95 "dd",
96 "shred",
97 "wipe",
98 "srm",
99 "unlink",
100 "chmod",
101 "chown",
102 "passwd",
103 "usermod",
104 "userdel",
105 "systemctl",
106 "service",
107 "kill",
108 "killall",
109 "pkill",
110 "reboot",
111 "shutdown",
112 "halt",
113 "poweroff",
114 "sudo",
115 "su",
116 "doas",
117 "runas",
118 "curl",
119 "wget",
120 "ftp",
121 "scp",
122 "rsync", "ssh",
124 "telnet",
125 "nc",
126 "ncat",
127 "socat", "mount",
129 "umount",
130 "fsck",
131 "tune2fs", "iptables",
133 "ufw",
134 "firewalld", "crontab",
136 "at", "docker",
138 "podman",
139 "kubectl", ];
141
142 if dangerous_commands.contains(&program.as_str()) {
143 return Err(anyhow::anyhow!(
144 "Dangerous command not allowed: '{}'. This command could potentially harm your system. \
145 Use file operation tools instead for safe file management.",
146 program
147 ));
148 }
149
150 let full_command = command_parts.join(" ");
152
153 if full_command.contains("rm -rf")
155 || full_command.contains("rm -r")
156 && (full_command.contains(" /") || full_command.contains(" ~"))
157 || full_command.contains("rmdir")
158 && (full_command.contains(" /") || full_command.contains(" ~"))
159 {
160 return Err(anyhow::anyhow!(
161 "Potentially dangerous recursive delete operation detected. \
162 Use file operation tools for safe file management."
163 ));
164 }
165
166 if full_command.contains("sudo ")
168 || full_command.contains("su ")
169 || full_command.contains("doas ")
170 || full_command.contains("runas ")
171 {
172 return Err(anyhow::anyhow!(
173 "Privilege escalation commands are not allowed. \
174 All operations run with current user privileges."
175 ));
176 }
177
178 if (full_command.contains("curl ") || full_command.contains("wget "))
180 && (full_command.contains("http://")
181 || full_command.contains("https://")
182 || full_command.contains("ftp://"))
183 {
184 return Err(anyhow::anyhow!(
185 "Network download commands are restricted. \
186 Use local file operations only."
187 ));
188 }
189
190 if full_command.contains(" > /etc/")
192 || full_command.contains(" >> /etc/")
193 || full_command.contains(" > /usr/")
194 || full_command.contains(" >> /usr/")
195 || full_command.contains(" > /var/")
196 || full_command.contains(" >> /var/")
197 {
198 return Err(anyhow::anyhow!(
199 "System configuration file modifications are not allowed. \
200 Use user-specific configuration files only."
201 ));
202 }
203
204 let sensitive_paths = [
206 "/etc/", "/usr/", "/var/", "/root/", "/boot/", "/sys/", "/proc/",
207 ];
208 for path in &sensitive_paths {
209 if full_command.contains(path)
210 && (full_command.contains("rm ")
211 || full_command.contains("mv ")
212 || full_command.contains("cp ")
213 || full_command.contains("chmod ")
214 || full_command.contains("chown "))
215 {
216 return Err(anyhow::anyhow!(
217 "Operations on system directories '{}' are not allowed. \
218 Work within your project workspace only.",
219 path.trim_end_matches('/')
220 ));
221 }
222 }
223
224 let allowed_commands = [
226 "ls", "pwd", "cat", "head", "tail", "grep", "find", "wc", "sort", "uniq", "cut", "awk",
227 "sed", "echo", "printf", "seq", "basename", "dirname", "date", "cal", "bc", "expr",
228 "test", "[", "]", "true", "false", "sleep", "which", "type", "file", "stat", "du",
229 "df", "ps", "top", "htop", "tree", "less", "more", "tac", "rev", "tr", "fold", "paste",
230 "join", "comm", "diff", "patch", "gzip", "gunzip", "bzip2", "bunzip2", "xz", "unxz",
231 "tar", "zip", "unzip", "gzip", "bzip2", "git", "hg",
232 "svn", "make", "cmake", "ninja", "cargo", "npm", "yarn", "pnpm", "python", "python3", "node", "ruby", "perl", "php", "java", "javac", "scala", "kotlin",
236 "go", "rustc", "gcc", "g++", "clang", "clang++", ];
238
239 if !allowed_commands.contains(&program.as_str()) {
240 return Err(anyhow::anyhow!(
241 "Command '{}' is not in the allowed commands list. \
242 Only safe development and analysis commands are permitted. \
243 Use specialized tools for file operations, searches, and builds.",
244 program
245 ));
246 }
247
248 Ok(())
249 }
250
251 async fn execute_ls(&self, args: Value) -> Result<Value> {
253 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
254 let show_hidden = args
255 .get("show_hidden")
256 .and_then(|v| v.as_bool())
257 .unwrap_or(false);
258
259 let mut cmd_args = vec![path.to_string()];
260 if show_hidden {
261 cmd_args.insert(0, "-la".to_string());
262 } else {
263 cmd_args.insert(0, "-l".to_string());
264 }
265
266 self.execute_pty_command("ls", cmd_args, Some(10)).await
267 }
268
269 async fn execute_pwd(&self) -> Result<Value> {
271 self.execute_pty_command("pwd", vec![], Some(5)).await
272 }
273
274 async fn execute_grep(&self, args: Value) -> Result<Value> {
276 let pattern = args
277 .get("pattern")
278 .and_then(|v| v.as_str())
279 .context("pattern is required for grep")?;
280
281 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
282 let recursive = args
283 .get("recursive")
284 .and_then(|v| v.as_bool())
285 .unwrap_or(false);
286
287 let mut cmd_args = vec![pattern.to_string(), path.to_string()];
288 if recursive {
289 cmd_args.insert(0, "-r".to_string());
290 }
291
292 self.execute_pty_command("grep", cmd_args, Some(30)).await
293 }
294
295 async fn execute_find(&self, args: Value) -> Result<Value> {
297 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
298 let name_pattern = args.get("name_pattern").and_then(|v| v.as_str());
299 let type_filter = args.get("type_filter").and_then(|v| v.as_str());
300
301 let mut cmd_args = vec![path.to_string()];
302 if let Some(pattern) = name_pattern {
303 cmd_args.push("-name".to_string());
304 cmd_args.push(pattern.to_string());
305 }
306 if let Some(filter) = type_filter {
307 cmd_args.push("-type".to_string());
308 cmd_args.push(filter.to_string());
309 }
310
311 self.execute_pty_command("find", cmd_args, Some(30)).await
312 }
313
314 async fn execute_cat(&self, args: Value) -> Result<Value> {
316 let path = args
317 .get("path")
318 .and_then(|v| v.as_str())
319 .context("path is required for cat")?;
320
321 let start_line = args.get("start_line").and_then(|v| v.as_u64());
322 let end_line = args.get("end_line").and_then(|v| v.as_u64());
323
324 if let (Some(start), Some(end)) = (start_line, end_line) {
325 let sed_cmd = format!("sed -n '{}','{}'p {}", start, end, path);
327 return self
328 .execute_pty_command("sh", vec!["-c".to_string(), sed_cmd], Some(10))
329 .await;
330 }
331
332 let cmd_args = vec![path.to_string()];
333
334 self.execute_pty_command("cat", cmd_args, Some(10)).await
335 }
336
337 async fn execute_head(&self, args: Value) -> Result<Value> {
339 let path = args
340 .get("path")
341 .and_then(|v| v.as_str())
342 .context("path is required for head")?;
343
344 let lines = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(10);
345
346 let cmd_args = vec!["-n".to_string(), lines.to_string(), path.to_string()];
347
348 self.execute_pty_command("head", cmd_args, Some(10)).await
349 }
350
351 async fn execute_tail(&self, args: Value) -> Result<Value> {
353 let path = args
354 .get("path")
355 .and_then(|v| v.as_str())
356 .context("path is required for tail")?;
357
358 let lines = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(10);
359
360 let cmd_args = vec!["-n".to_string(), lines.to_string(), path.to_string()];
361
362 self.execute_pty_command("tail", cmd_args, Some(10)).await
363 }
364
365 async fn execute_mkdir(&self, args: Value) -> Result<Value> {
367 let path = args
368 .get("path")
369 .and_then(|v| v.as_str())
370 .context("path is required for mkdir")?;
371
372 let parents = args
373 .get("parents")
374 .and_then(|v| v.as_bool())
375 .unwrap_or(false);
376
377 let mut cmd_args = vec![path.to_string()];
378 if parents {
379 cmd_args.insert(0, "-p".to_string());
380 }
381
382 self.execute_pty_command("mkdir", cmd_args, Some(10)).await
383 }
384
385 async fn execute_rm(&self, args: Value) -> Result<Value> {
387 let path = args
388 .get("path")
389 .and_then(|v| v.as_str())
390 .context("path is required for rm")?;
391
392 let recursive = args
393 .get("recursive")
394 .and_then(|v| v.as_bool())
395 .unwrap_or(false);
396 let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
397
398 let mut cmd_args = vec![];
399 if recursive {
400 cmd_args.push("-r".to_string());
401 }
402 if force {
403 cmd_args.push("-f".to_string());
404 }
405 cmd_args.push(path.to_string());
406
407 self.execute_pty_command("rm", cmd_args, Some(10)).await
408 }
409
410 async fn execute_cp(&self, args: Value) -> Result<Value> {
412 let source = args
413 .get("source")
414 .and_then(|v| v.as_str())
415 .context("source is required for cp")?;
416
417 let dest = args
418 .get("dest")
419 .and_then(|v| v.as_str())
420 .context("dest is required for cp")?;
421
422 let recursive = args
423 .get("recursive")
424 .and_then(|v| v.as_bool())
425 .unwrap_or(false);
426
427 let mut cmd_args = vec![];
428 if recursive {
429 cmd_args.push("-r".to_string());
430 }
431 cmd_args.push(source.to_string());
432 cmd_args.push(dest.to_string());
433
434 self.execute_pty_command("cp", cmd_args, Some(30)).await
435 }
436
437 async fn execute_mv(&self, args: Value) -> Result<Value> {
439 let source = args
440 .get("source")
441 .and_then(|v| v.as_str())
442 .context("source is required for mv")?;
443
444 let dest = args
445 .get("dest")
446 .and_then(|v| v.as_str())
447 .context("dest is required for mv")?;
448
449 let cmd_args = vec![source.to_string(), dest.to_string()];
450
451 self.execute_pty_command("mv", cmd_args, Some(10)).await
452 }
453
454 async fn execute_stat(&self, args: Value) -> Result<Value> {
456 let path = args
457 .get("path")
458 .and_then(|v| v.as_str())
459 .context("path is required for stat")?;
460
461 let cmd_args = vec!["-la".to_string(), path.to_string()];
462
463 self.execute_pty_command("ls", cmd_args, Some(10)).await
464 }
465
466 async fn execute_run(&self, args: Value) -> Result<Value> {
468 let command = args
469 .get("command")
470 .and_then(|v| v.as_str())
471 .context("command is required for run")?;
472
473 let cmd_args = args
474 .get("args")
475 .and_then(|v| v.as_array())
476 .map(|arr| {
477 arr.iter()
478 .filter_map(|v| v.as_str())
479 .map(|s| s.to_string())
480 .collect::<Vec<String>>()
481 })
482 .unwrap_or_default();
483
484 self.execute_pty_command(command, cmd_args, Some(30)).await
485 }
486}
487
488#[async_trait]
489impl Tool for BashTool {
490 async fn execute(&self, args: Value) -> Result<Value> {
491 let command = args
492 .get("bash_command")
493 .and_then(|v| v.as_str())
494 .unwrap_or("ls");
495
496 match command {
497 "ls" => self.execute_ls(args).await,
498 "pwd" => self.execute_pwd().await,
499 "grep" => self.execute_grep(args).await,
500 "find" => self.execute_find(args).await,
501 "cat" => self.execute_cat(args).await,
502 "head" => self.execute_head(args).await,
503 "tail" => self.execute_tail(args).await,
504 "mkdir" => self.execute_mkdir(args).await,
505 "rm" => self.execute_rm(args).await,
506 "cp" => self.execute_cp(args).await,
507 "mv" => self.execute_mv(args).await,
508 "stat" => self.execute_stat(args).await,
509 "run" => self.execute_run(args).await,
510 _ => Err(anyhow::anyhow!("Unknown bash command: {}", command)),
511 }
512 }
513
514 fn name(&self) -> &'static str {
515 tools::BASH
516 }
517
518 fn description(&self) -> &'static str {
519 "Bash-like commands with security validation: ls, pwd, grep, find, cat, head, tail, mkdir, rm, cp, mv, stat, run. \
520 Dangerous commands (rm, sudo, network operations, system modifications) are blocked for safety."
521 }
522}