Skip to main content

skill_tools/
bash.rs

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(&params.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}