Skip to main content

trae_agent_rs_core/tools/utils/
run.rs

1//! Tool execution utilities
2
3use crate::error::Result;
4use std::collections::HashMap;
5use std::process::Stdio;
6use tokio::io::{AsyncBufReadExt, BufReader};
7use tokio::process::{Child, Command};
8use tokio::time::{timeout, Duration, Instant};
9
10/// Command execution options
11#[derive(Debug, Clone)]
12pub struct CommandOptions {
13    pub timeout_seconds: Option<u64>,
14    pub truncate_after: Option<usize>,
15    pub working_directory: Option<String>,
16    pub environment: HashMap<String, String>,
17    pub capture_stderr: bool,
18    pub shell: Option<String>,
19}
20
21impl Default for CommandOptions {
22    fn default() -> Self {
23        Self {
24            timeout_seconds: Some(120),
25            truncate_after: Some(16000),
26            working_directory: None,
27            environment: HashMap::new(),
28            capture_stderr: true,
29            shell: Some("/bin/bash".to_string()),
30        }
31    }
32}
33
34/// Command execution result
35#[derive(Debug, Clone)]
36pub struct CommandResult {
37    pub exit_code: i32,
38    pub stdout: String,
39    pub stderr: String,
40    pub duration_ms: u64,
41    pub timed_out: bool,
42    pub truncated: bool,
43}
44
45/// Execute a command with comprehensive options
46pub async fn execute_command(command: &str, options: CommandOptions) -> Result<CommandResult> {
47    let start_time = Instant::now();
48    
49    let mut cmd = if let Some(shell) = &options.shell {
50        let mut cmd = Command::new(shell);
51        cmd.arg("-c").arg(command);
52        cmd
53    } else {
54        // Parse command and arguments
55        let parts: Vec<&str> = command.split_whitespace().collect();
56        if parts.is_empty() {
57            return Err("Empty command".into());
58        }
59        
60        let mut cmd = Command::new(parts[0]);
61        if parts.len() > 1 {
62            cmd.args(&parts[1..]);
63        }
64        cmd
65    };
66
67    // Set working directory
68    if let Some(working_dir) = &options.working_directory {
69        cmd.current_dir(working_dir);
70    }
71
72    // Set environment variables
73    for (key, value) in &options.environment {
74        cmd.env(key, value);
75    }
76
77    // Configure stdio
78    cmd.stdin(Stdio::piped())
79       .stdout(Stdio::piped());
80    
81    if options.capture_stderr {
82        cmd.stderr(Stdio::piped());
83    } else {
84        cmd.stderr(Stdio::inherit());
85    }
86
87    let mut child = cmd.spawn()?;
88    
89    // Execute with timeout
90    let timeout_duration = Duration::from_secs(options.timeout_seconds.unwrap_or(120));
91    let result = timeout(timeout_duration, async {
92        execute_child(&mut child, options.capture_stderr).await
93    }).await;
94
95    let duration = start_time.elapsed();
96    
97    match result {
98        Ok(Ok((exit_code, stdout, stderr))) => {
99            let truncate_limit = options.truncate_after.unwrap_or(16000);
100            let (stdout_truncated, stdout_final) = truncate_output(&stdout, truncate_limit);
101            let (stderr_truncated, stderr_final) = truncate_output(&stderr, truncate_limit);
102            
103            Ok(CommandResult {
104                exit_code,
105                stdout: stdout_final,
106                stderr: stderr_final,
107                duration_ms: duration.as_millis() as u64,
108                timed_out: false,
109                truncated: stdout_truncated || stderr_truncated,
110            })
111        }
112        Ok(Err(e)) => Err(e),
113        Err(_) => {
114            // Kill the process if it's still running
115            let _ = child.kill().await;
116            
117            Ok(CommandResult {
118                exit_code: -1,
119                stdout: String::new(),
120                stderr: format!("Command timed out after {} seconds", timeout_duration.as_secs()),
121                duration_ms: duration.as_millis() as u64,
122                timed_out: true,
123                truncated: false,
124            })
125        }
126    }
127}
128
129/// Execute child process and capture output
130async fn execute_child(child: &mut Child, capture_stderr: bool) -> Result<(i32, String, String)> {
131    let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
132    let stderr = if capture_stderr {
133        child.stderr.take()
134    } else {
135        None
136    };
137
138    let mut stdout_reader = BufReader::new(stdout);
139    let mut stdout_lines = Vec::new();
140    let mut stderr_lines = Vec::new();
141
142    // Read stdout
143    let stdout_task = async {
144        let mut line = String::new();
145        while stdout_reader.read_line(&mut line).await? > 0 {
146            stdout_lines.push(line.clone());
147            line.clear();
148        }
149        Ok::<(), std::io::Error>(())
150    };
151
152    // Read stderr if capturing
153    let stderr_task = async {
154        if let Some(stderr) = stderr {
155            let mut stderr_reader = BufReader::new(stderr);
156            let mut line = String::new();
157            while stderr_reader.read_line(&mut line).await? > 0 {
158                stderr_lines.push(line.clone());
159                line.clear();
160            }
161        }
162        Ok::<(), std::io::Error>(())
163    };
164
165    // Wait for both tasks to complete
166    let (stdout_result, stderr_result) = tokio::join!(stdout_task, stderr_task);
167    stdout_result?;
168    stderr_result?;
169
170    // Wait for process to exit
171    let status = child.wait().await?;
172    let exit_code = status.code().unwrap_or(-1);
173
174    let stdout_output = stdout_lines.join("");
175    let stderr_output = stderr_lines.join("");
176
177    Ok((exit_code, stdout_output, stderr_output))
178}
179
180/// Truncate output if it exceeds the limit
181fn truncate_output(output: &str, limit: usize) -> (bool, String) {
182    if output.len() <= limit {
183        (false, output.to_string())
184    } else {
185        let truncated = format!(
186            "{}\n\n<output truncated after {} characters>\n\
187             <NOTE>To see the full output, increase the truncate_after limit or \
188             redirect output to a file.</NOTE>",
189            &output[..limit],
190            limit
191        );
192        (true, truncated)
193    }
194}
195
196/// Stream command output in real-time
197pub async fn stream_command(
198    command: &str,
199    options: CommandOptions,
200    mut output_handler: impl FnMut(&str) -> Result<()>,
201) -> Result<CommandResult> {
202    let start_time = Instant::now();
203    
204    let mut cmd = if let Some(shell) = &options.shell {
205        let mut cmd = Command::new(shell);
206        cmd.arg("-c").arg(command);
207        cmd
208    } else {
209        let parts: Vec<&str> = command.split_whitespace().collect();
210        if parts.is_empty() {
211            return Err("Empty command".into());
212        }
213        
214        let mut cmd = Command::new(parts[0]);
215        if parts.len() > 1 {
216            cmd.args(&parts[1..]);
217        }
218        cmd
219    };
220
221    if let Some(working_dir) = &options.working_directory {
222        cmd.current_dir(working_dir);
223    }
224
225    for (key, value) in &options.environment {
226        cmd.env(key, value);
227    }
228
229    cmd.stdin(Stdio::piped())
230       .stdout(Stdio::piped())
231       .stderr(Stdio::piped());
232
233    let mut child = cmd.spawn()?;
234    
235    let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
236    let stderr = child.stderr.take().ok_or("Failed to capture stderr")?;
237
238    let mut stdout_reader = BufReader::new(stdout);
239    let mut stderr_reader = BufReader::new(stderr);
240    
241    let mut all_output = String::new();
242    let mut exit_code = 0;
243    let mut timed_out = false;
244
245    let timeout_duration = Duration::from_secs(options.timeout_seconds.unwrap_or(120));
246    let result = timeout(timeout_duration, async {
247        let mut stdout_line = String::new();
248        let mut stderr_line = String::new();
249
250        loop {
251            tokio::select! {
252                result = stdout_reader.read_line(&mut stdout_line) => {
253                    match result {
254                        Ok(0) => break, // EOF
255                        Ok(_) => {
256                            output_handler(&stdout_line)?;
257                            all_output.push_str(&stdout_line);
258                            stdout_line.clear();
259                        }
260                        Err(e) => return Err(e.into()),
261                    }
262                }
263                result = stderr_reader.read_line(&mut stderr_line) => {
264                    match result {
265                        Ok(0) => {}, // EOF on stderr
266                        Ok(_) => {
267                            output_handler(&stderr_line)?;
268                            all_output.push_str(&stderr_line);
269                            stderr_line.clear();
270                        }
271                        Err(e) => return Err(e.into()),
272                    }
273                }
274                status = child.wait() => {
275                    exit_code = status?.code().unwrap_or(-1);
276                    break;
277                }
278            }
279        }
280
281        Ok::<(), crate::error::Error>(())
282    }).await;
283
284    let duration = start_time.elapsed();
285
286    match result {
287        Ok(Ok(())) => {},
288        Ok(Err(e)) => return Err(e),
289        Err(_) => {
290            let _ = child.kill().await;
291            timed_out = true;
292            exit_code = -1;
293        }
294    }
295
296    let truncate_limit = options.truncate_after.unwrap_or(16000);
297    let (truncated, final_output) = truncate_output(&all_output, truncate_limit);
298
299    Ok(CommandResult {
300        exit_code,
301        stdout: final_output,
302        stderr: String::new(), // Combined with stdout in streaming mode
303        duration_ms: duration.as_millis() as u64,
304        timed_out,
305        truncated,
306    })
307}
308
309/// Validate command safety (basic checks)
310pub fn validate_command_safety(command: &str) -> Result<()> {
311    let dangerous_patterns = [
312        "rm -rf /",
313        ":(){ :|:& };:",  // Fork bomb
314        "dd if=/dev/zero",
315        "mkfs.",
316        "format ",
317        "> /dev/",
318        "chmod 777 /",
319        "chown root /",
320    ];
321
322    let command_lower = command.to_lowercase();
323    for pattern in &dangerous_patterns {
324        if command_lower.contains(pattern) {
325            return Err(format!("Potentially dangerous command detected: {}", pattern).into());
326        }
327    }
328
329    Ok(())
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[tokio::test]
337    async fn test_simple_command() {
338        let options = CommandOptions::default();
339        let result = execute_command("echo 'Hello, World!'", options).await.unwrap();
340        
341        assert_eq!(result.exit_code, 0);
342        assert!(result.stdout.contains("Hello, World!"));
343        assert!(!result.timed_out);
344    }
345
346    #[tokio::test]
347    async fn test_command_timeout() {
348        let mut options = CommandOptions::default();
349        options.timeout_seconds = Some(1);
350        
351        let result = execute_command("sleep 5", options).await.unwrap();
352        
353        assert!(result.timed_out);
354        assert_eq!(result.exit_code, -1);
355    }
356
357    #[test]
358    fn test_output_truncation() {
359        let long_output = "a".repeat(20000);
360        let (truncated, output) = truncate_output(&long_output, 1000);
361        
362        assert!(truncated);
363        assert!(output.len() > 1000); // Includes truncation message
364        assert!(output.contains("output truncated"));
365    }
366
367    #[test]
368    fn test_command_safety_validation() {
369        assert!(validate_command_safety("echo hello").is_ok());
370        assert!(validate_command_safety("rm -rf /").is_err());
371        assert!(validate_command_safety("dd if=/dev/zero of=/dev/sda").is_err());
372    }
373}