vtcode_core/tools/
command.rs1use 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#[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 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 ("sh", vec!["-c".to_string(), full_command])
49 } else {
50 (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 program == "sh" && command.len() >= 3 && command[1] == "-c" {
104 let actual_command = &command[2];
105
106 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 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 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}