vtcode_core/tools/
bash_tool.rs

1//! Bash-like tool for command execution
2//!
3//! This tool provides bash-like functionality for running common
4//! commands and tools that require a shell environment.
5
6use 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/// Bash-like tool for command execution
15#[derive(Clone)]
16pub struct BashTool {
17    workspace_root: PathBuf,
18}
19
20impl BashTool {
21    /// Create a new bash tool
22    pub fn new(workspace_root: PathBuf) -> Self {
23        Self { workspace_root }
24    }
25
26    /// Execute command and capture its output
27    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    /// Validate command for security
80    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        // Basic security checks - dangerous commands that should be blocked
88        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", // Network commands
123            "ssh",
124            "telnet",
125            "nc",
126            "ncat",
127            "socat", // Remote access
128            "mount",
129            "umount",
130            "fsck",
131            "tune2fs", // Filesystem operations
132            "iptables",
133            "ufw",
134            "firewalld", // Firewall
135            "crontab",
136            "at", // Scheduling
137            "docker",
138            "podman",
139            "kubectl", // Container/orchestration
140        ];
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        // Check for suspicious patterns in the full command
151        let full_command = command_parts.join(" ");
152
153        // Block recursive delete operations
154        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        // Block privilege escalation attempts
167        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        // Block network operations that could exfiltrate data
179        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        // Block commands that modify system configuration
191        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        // Block commands that access sensitive directories
205        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        // Allow only safe commands that are commonly needed for development
225        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", // Version control (read-only operations)
233            "make", "cmake", "ninja", // Build systems
234            "cargo", "npm", "yarn", "pnpm", // Package managers
235            "python", "python3", "node", "ruby", "perl", "php", "java", "javac", "scala", "kotlin",
236            "go", "rustc", "gcc", "g++", "clang", "clang++", // Compilers
237        ];
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    /// Execute ls command
252    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    /// Execute pwd command
270    async fn execute_pwd(&self) -> Result<Value> {
271        self.execute_pty_command("pwd", vec![], Some(5)).await
272    }
273
274    /// Execute grep command
275    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    /// Execute find command
296    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    /// Execute cat command
315    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            // Use sed to extract line range
326            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    /// Execute head command
338    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    /// Execute tail command
352    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    /// Execute mkdir command
366    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    /// Execute rm command
386    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    /// Execute cp command
411    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    /// Execute mv command
438    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    /// Execute stat command
455    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    /// Execute arbitrary command
467    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}