llm_coding_tools_rig/
bash.rs

1//! Shell command execution tool.
2//!
3//! Provides cross-platform shell command execution with timeout support.
4
5use llm_coding_tools_core::operations::execute_command;
6use llm_coding_tools_core::tool_names;
7use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput};
8use rig::completion::ToolDefinition;
9use rig::tool::Tool;
10use schemars::{schema_for, JsonSchema};
11use serde::Deserialize;
12use std::path::Path;
13use std::time::Duration;
14
15/// Default timeout: 2 minutes.
16const DEFAULT_TIMEOUT_MS: u64 = 120_000;
17
18fn default_timeout_ms() -> u64 {
19    DEFAULT_TIMEOUT_MS
20}
21
22/// Arguments for the bash tool.
23#[derive(Debug, Clone, Deserialize, JsonSchema)]
24pub struct BashArgs {
25    /// The shell command to execute.
26    pub command: String,
27    /// Optional working directory (must be absolute path).
28    pub workdir: Option<String>,
29    /// Timeout in milliseconds (default: 120000).
30    #[serde(default = "default_timeout_ms")]
31    pub timeout_ms: u64,
32}
33
34/// Tool for executing shell commands.
35///
36/// Uses bash on Unix, cmd on Windows.
37#[derive(Debug, Clone, Copy, Default)]
38pub struct BashTool;
39
40impl BashTool {
41    /// Creates a new bash tool instance.
42    #[inline]
43    pub fn new() -> Self {
44        Self
45    }
46}
47
48impl Tool for BashTool {
49    const NAME: &'static str = tool_names::BASH;
50
51    type Error = ToolError;
52    type Args = BashArgs;
53    type Output = ToolOutput;
54
55    async fn definition(&self, _prompt: String) -> ToolDefinition {
56        ToolDefinition {
57            name: <Self as Tool>::NAME.to_string(),
58            description: "Execute a shell command with optional working directory and timeout."
59                .to_string(),
60            parameters: serde_json::to_value(schema_for!(BashArgs))
61                .expect("schema serialization should never fail"),
62        }
63    }
64
65    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
66        let workdir = args.workdir.as_ref().map(Path::new);
67        let timeout = Duration::from_millis(args.timeout_ms);
68
69        let result = execute_command(&args.command, workdir, timeout).await?;
70        Ok(result.format_output())
71    }
72}
73
74impl ToolContext for BashTool {
75    const NAME: &'static str = tool_names::BASH;
76
77    fn context(&self) -> &'static str {
78        llm_coding_tools_core::context::BASH
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[tokio::test]
87    async fn executes_echo() {
88        let tool = BashTool::new();
89        let args = BashArgs {
90            command: "echo hello".to_string(),
91            workdir: None,
92            timeout_ms: 5000,
93        };
94        let result = tool.call(args).await.unwrap();
95        assert!(result.content.contains("hello"));
96    }
97
98    #[tokio::test]
99    async fn timeout_returns_error() {
100        let tool = BashTool::new();
101        let cmd = if cfg!(target_os = "windows") {
102            "ping -n 10 127.0.0.1"
103        } else {
104            "sleep 10"
105        };
106        let args = BashArgs {
107            command: cmd.to_string(),
108            workdir: None,
109            timeout_ms: 100,
110        };
111        let result = tool.call(args).await;
112        assert!(matches!(result, Err(ToolError::Timeout(_))));
113    }
114}