vtcode_core/tools/
command.rs

1//! Command execution tool
2
3use super::traits::{ModeTool, Tool};
4use super::types::*;
5use crate::config::constants::tools;
6use anyhow::{Context, Result, anyhow};
7use async_trait::async_trait;
8use serde_json::{Value, json};
9use std::{path::PathBuf, process::Stdio, time::Duration};
10use tokio::{process::Command, time::timeout};
11
12/// Command execution tool using standard process handling
13#[derive(Clone)]
14pub struct CommandTool {
15    workspace_root: PathBuf,
16}
17
18impl CommandTool {
19    pub fn new(workspace_root: PathBuf) -> Self {
20        Self { workspace_root }
21    }
22
23    async fn execute_terminal_command(&self, input: &EnhancedTerminalInput) -> Result<Value> {
24        if input.command.is_empty() {
25            return Err(anyhow!("command array cannot be empty"));
26        }
27
28        // Check if command contains shell metacharacters that require shell interpretation
29        let full_command = input.command.join(" ");
30        let has_shell_metacharacters = full_command.contains('|')
31            || full_command.contains('>')
32            || full_command.contains('<')
33            || full_command.contains('&')
34            || full_command.contains(';')
35            || full_command.contains('(')
36            || full_command.contains(')')
37            || full_command.contains('$')
38            || full_command.contains('`')
39            || full_command.contains('*')
40            || full_command.contains('?')
41            || full_command.contains('[')
42            || full_command.contains(']')
43            || full_command.contains('{')
44            || full_command.contains('}');
45
46        let (program, args) = if has_shell_metacharacters {
47            // Use shell to interpret metacharacters
48            ("sh", vec!["-c".to_string(), full_command])
49        } else {
50            // Execute directly
51            (input.command[0].as_str(), input.command[1..].to_vec())
52        };
53
54        let mut cmd = Command::new(program);
55        cmd.args(&args);
56
57        let work_dir = if let Some(ref working_dir) = input.working_dir {
58            self.workspace_root.join(working_dir)
59        } else {
60            self.workspace_root.clone()
61        };
62
63        cmd.current_dir(work_dir);
64        cmd.stdout(Stdio::piped());
65        cmd.stderr(Stdio::piped());
66
67        let duration = Duration::from_secs(input.timeout_secs.unwrap_or(30));
68        let command_str = input.command.join(" ");
69        let output = timeout(duration, cmd.output())
70            .await
71            .with_context(|| {
72                format!(
73                    "command '{}' timed out after {}s",
74                    command_str,
75                    duration.as_secs()
76                )
77            })?
78            .with_context(|| format!("failed to run command: {}", command_str))?;
79        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
80        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
81
82        Ok(json!({
83            "success": output.status.success(),
84            "exit_code": output.status.code().unwrap_or_default(),
85            "stdout": stdout,
86            "stderr": stderr,
87            "mode": "terminal",
88            "pty_enabled": false,
89            "command": command_str,
90            "used_shell": has_shell_metacharacters
91        }))
92    }
93
94    fn validate_command(&self, command: &[String]) -> Result<()> {
95        if command.is_empty() {
96            return Err(anyhow!("Command cannot be empty"));
97        }
98
99        let program = &command[0];
100        let full_command = command.join(" ");
101
102        // If this is a shell command (sh -c), validate the actual command being executed
103        if program == "sh" && command.len() >= 3 && command[1] == "-c" {
104            let actual_command = &command[2];
105
106            // Check for extremely dangerous patterns even in shell commands
107            if actual_command.contains("rm -rf /")
108                || actual_command.contains("sudo rm")
109                || actual_command.contains("format")
110                || actual_command.contains("fdisk")
111                || actual_command.contains("mkfs")
112            {
113                return Err(anyhow!(
114                    "Potentially dangerous command pattern detected in shell command"
115                ));
116            }
117
118            return Ok(());
119        }
120
121        // For direct commands, check the program name
122        let dangerous_commands = ["rm", "rmdir", "del", "format", "fdisk", "mkfs", "dd"];
123        if dangerous_commands.contains(&program.as_str()) {
124            return Err(anyhow!("Dangerous command not allowed: {}", program));
125        }
126
127        // Check for dangerous patterns in the full command
128        if full_command.contains("rm -rf /") || full_command.contains("sudo rm") {
129            return Err(anyhow!("Potentially dangerous command pattern detected"));
130        }
131
132        Ok(())
133    }
134}
135
136#[async_trait]
137impl Tool for CommandTool {
138    async fn execute(&self, args: Value) -> Result<Value> {
139        let input: EnhancedTerminalInput = serde_json::from_value(args)?;
140        self.validate_command(&input.command)?;
141        self.execute_terminal_command(&input).await
142    }
143
144    fn name(&self) -> &'static str {
145        tools::RUN_TERMINAL_CMD
146    }
147
148    fn description(&self) -> &'static str {
149        "Execute terminal commands"
150    }
151
152    fn validate_args(&self, args: &Value) -> Result<()> {
153        let input: EnhancedTerminalInput = serde_json::from_value(args.clone())?;
154        self.validate_command(&input.command)
155    }
156}
157
158#[async_trait]
159impl ModeTool for CommandTool {
160    fn supported_modes(&self) -> Vec<&'static str> {
161        vec!["terminal"]
162    }
163
164    async fn execute_mode(&self, mode: &str, args: Value) -> Result<Value> {
165        let input: EnhancedTerminalInput = serde_json::from_value(args)?;
166        match mode {
167            "terminal" => self.execute_terminal_command(&input).await,
168            _ => Err(anyhow!("Unsupported command execution mode: {}", mode)),
169        }
170    }
171}