toast_api/tools/
bash.rs

1//! Bash tool implementation for executing shell commands
2
3use super::{Tool, ToolInfo};
4use anyhow::{anyhow, Result};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::process::Stdio;
8use tokio::process::Command;
9use tokio::time::{timeout, Duration};
10
11#[derive(Debug, Clone)]
12pub struct BashTool {
13    timeout_secs: u64,
14}
15
16#[derive(Debug, Deserialize, Serialize)]
17struct BashParams {
18    command: String,
19}
20
21impl Default for BashTool {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl BashTool {
28    pub fn new() -> Self {
29        Self { timeout_secs: 120 }
30    }
31}
32
33#[async_trait]
34impl Tool for BashTool {
35    fn info(&self) -> ToolInfo {
36        ToolInfo {
37            name: "bash".to_string(),
38            description: r#"Run commands in a bash shell
39* State is persistent across command calls within the same session
40* You don't have access to the internet via this tool
41* Long-running commands will timeout after 120 seconds
42* For background processes, use '&' (e.g., 'sleep 10 &')
43* To inspect specific line ranges, use 'sed -n 10,25p /path/to/file'"#.to_string(),
44            input_schema: serde_json::json!({
45                "type": "object",
46                "properties": {
47                    "command": {
48                        "type": "string",
49                        "description": "The bash command to run"
50                    }
51                },
52                "required": ["command"]
53            }),
54        }
55    }
56
57    async fn execute(&self, params: serde_json::Value) -> Result<String> {
58        let bash_params: BashParams = serde_json::from_value(params)
59            .map_err(|e| anyhow!("Invalid parameters: {}", e))?;
60
61        // Execute command with timeout
62        let output = timeout(
63            Duration::from_secs(self.timeout_secs),
64            Command::new("bash")
65                .arg("-c")
66                .arg(&bash_params.command)
67                .stdout(Stdio::piped())
68                .stderr(Stdio::piped())
69                .output()
70        )
71        .await
72        .map_err(|_| anyhow!("Command timed out after {} seconds", self.timeout_secs))??;
73
74        let mut result = String::new();
75
76        // Add stdout if present
77        if !output.stdout.is_empty() {
78            result.push_str("=== STDOUT ===\n");
79            result.push_str(&String::from_utf8_lossy(&output.stdout));
80            if !result.ends_with('\n') {
81                result.push('\n');
82            }
83        }
84
85        // Add stderr if present
86        if !output.stderr.is_empty() {
87            if !result.is_empty() {
88                result.push('\n');
89            }
90            result.push_str("=== STDERR ===\n");
91            result.push_str(&String::from_utf8_lossy(&output.stderr));
92            if !result.ends_with('\n') {
93                result.push('\n');
94            }
95        }
96
97        // Add exit code
98        if !result.is_empty() {
99            result.push('\n');
100        }
101        result.push_str(&format!("Exit code: {}", output.status.code().unwrap_or(-1)));
102
103        Ok(result)
104    }
105}