1use crate::{ToolDefinition, ToolError, ToolResult};
2use serde::Deserialize;
3use std::process::Stdio;
4use tokio::process::Command;
5use tracing::{debug, info, warn};
6
7#[derive(Debug, Deserialize)]
8pub struct BashParams {
9 pub command: String,
10 #[serde(default)]
11 pub timeout: Option<String>,
12 #[serde(default)]
13 pub workdir: Option<String>,
14}
15
16impl BashParams {
17 pub fn timeout_ms(&self) -> Option<u64> {
18 self.timeout.as_ref().and_then(|s| s.parse().ok())
19 }
20}
21
22#[derive(Debug, Clone)]
23pub struct BashTool;
24
25impl BashTool {
26 pub fn new() -> Self {
27 Self
28 }
29
30 pub fn definition(&self) -> ToolDefinition {
31 ToolDefinition {
32 name: "bash".to_string(),
33 description: "Execute a shell command".to_string(),
34 parameters: serde_json::json!({
35 "type": "object",
36 "properties": {
37 "command": {
38 "type": "string",
39 "description": "The shell command to execute"
40 },
41 "timeout": {
42 "type": "number",
43 "description": "Timeout in milliseconds (optional)"
44 },
45 "workdir": {
46 "type": "string",
47 "description": "Working directory (optional)"
48 }
49 },
50 "required": ["command"]
51 }),
52 }
53 }
54
55 pub async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError> {
56 info!("Bash tool executing with params: {:?}", params);
57
58 let params: BashParams = serde_json::from_value(params).map_err(|e| {
59 warn!("Bash tool failed to parse params: {}", e);
60 ToolError::InvalidParameters(e.to_string())
61 })?;
62
63 debug!(
64 "Parsed bash command: {}, timeout: {:?}, workdir: {:?}",
65 params.command, params.timeout, params.workdir
66 );
67
68 let mut cmd = Command::new("bash");
69 cmd.arg("-c")
70 .arg(¶ms.command)
71 .stdout(Stdio::piped())
72 .stderr(Stdio::piped());
73
74 if let Some(ref workdir) = params.workdir {
75 debug!("Setting workdir to: {}", workdir);
76 cmd.current_dir(workdir);
77 }
78
79 let output = if let Some(timeout) = params.timeout_ms() {
80 debug!("Executing with timeout: {}ms", timeout);
81 match tokio::time::timeout(std::time::Duration::from_millis(timeout), cmd.output())
82 .await
83 {
84 Ok(Ok(output)) => {
85 debug!("Bash command completed successfully");
86 output
87 }
88 Ok(Err(e)) => {
89 warn!("Bash command execution error: {}", e);
90 return Ok(ToolResult {
91 success: false,
92 output: String::new(),
93 error: Some(format!("Command failed: {}", e)),
94 });
95 }
96 Err(_) => {
97 warn!("Bash command timed out after {}ms", timeout);
98 return Ok(ToolResult {
99 success: false,
100 output: String::new(),
101 error: Some(format!("Command timed out after {}ms", timeout)),
102 });
103 }
104 }
105 } else {
106 debug!("Executing without timeout");
107 match cmd.output().await {
108 Ok(output) => {
109 debug!("Bash command completed");
110 output
111 }
112 Err(e) => {
113 warn!("Bash command spawn error: {}", e);
114 return Ok(ToolResult {
115 success: false,
116 output: String::new(),
117 error: Some(format!("Failed to execute: {}", e)),
118 });
119 }
120 }
121 };
122
123 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
124 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
125
126 let success = output.status.success();
127 debug!(
128 "Bash exit status: {}, stdout_len: {}, stderr_len: {}",
129 output.status,
130 stdout.len(),
131 stderr.len()
132 );
133
134 let output_str = if stderr.is_empty() {
135 stdout
136 } else {
137 format!("{}\n--- stderr ---\n{}", stdout, stderr)
138 };
139
140 let result = ToolResult {
141 success,
142 output: output_str,
143 error: if success { None } else { Some(stderr) },
144 };
145
146 info!(
147 "Bash tool result: success={}, output_len={}",
148 result.success,
149 result.output.len()
150 );
151 Ok(result)
152 }
153}