Skip to main content

composio_sdk/meta_tools/
bash.rs

1//! Bash Executor Implementation
2//!
3//! Native Rust implementation of COMPOSIO_REMOTE_BASH_TOOL meta tool.
4//! Executes bash commands in an isolated sandbox environment.
5
6use crate::error::ComposioError;
7use std::path::PathBuf;
8use std::process::Stdio;
9use tokio::process::Command;
10
11/// Bash execution result
12#[derive(Debug, Clone)]
13pub struct BashResult {
14    /// Standard output
15    pub stdout: String,
16    
17    /// Standard error
18    pub stderr: String,
19    
20    /// Exit code
21    pub exit_code: i32,
22    
23    /// Execution time in milliseconds
24    pub execution_time_ms: u128,
25}
26
27/// Bash executor with sandboxing
28pub struct BashExecutor {
29    /// Sandbox directory for command execution
30    sandbox_dir: PathBuf,
31    
32    /// Timeout in seconds (default: 30)
33    timeout_secs: u64,
34    
35    /// Environment variables
36    env_vars: Vec<(String, String)>,
37}
38
39impl BashExecutor {
40    /// Create a new bash executor with default sandbox
41    ///
42    /// # Example
43    ///
44    /// ```no_run
45    /// use composio_sdk::meta_tools::BashExecutor;
46    ///
47    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
48    /// let executor = BashExecutor::new();
49    /// let result = executor.execute("ls -la").await?;
50    /// println!("Output: {}", result.stdout);
51    /// # Ok(())
52    /// # }
53    /// ```
54    pub fn new() -> Self {
55        Self {
56            sandbox_dir: std::env::temp_dir().join("composio_sandbox"),
57            timeout_secs: 30,
58            env_vars: Vec::new(),
59        }
60    }
61
62    /// Create a bash executor with custom sandbox directory
63    ///
64    /// # Arguments
65    ///
66    /// * `sandbox_dir` - Directory to use as sandbox
67    ///
68    /// # Example
69    ///
70    /// ```no_run
71    /// use composio_sdk::meta_tools::BashExecutor;
72    /// use std::path::PathBuf;
73    ///
74    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
75    /// let executor = BashExecutor::with_sandbox(PathBuf::from("/tmp/my_sandbox"));
76    /// # Ok(())
77    /// # }
78    /// ```
79    pub fn with_sandbox(sandbox_dir: PathBuf) -> Self {
80        Self {
81            sandbox_dir,
82            timeout_secs: 30,
83            env_vars: Vec::new(),
84        }
85    }
86
87    /// Set execution timeout
88    ///
89    /// # Arguments
90    ///
91    /// * `timeout_secs` - Timeout in seconds
92    ///
93    /// # Example
94    ///
95    /// ```no_run
96    /// use composio_sdk::meta_tools::BashExecutor;
97    ///
98    /// let executor = BashExecutor::new().timeout(60);
99    /// ```
100    pub fn timeout(mut self, timeout_secs: u64) -> Self {
101        self.timeout_secs = timeout_secs;
102        self
103    }
104
105    /// Add environment variable
106    ///
107    /// # Arguments
108    ///
109    /// * `key` - Variable name
110    /// * `value` - Variable value
111    ///
112    /// # Example
113    ///
114    /// ```no_run
115    /// use composio_sdk::meta_tools::BashExecutor;
116    ///
117    /// let executor = BashExecutor::new()
118    ///     .env("PATH", "/usr/local/bin:/usr/bin")
119    ///     .env("HOME", "/tmp");
120    /// ```
121    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
122        self.env_vars.push((key.into(), value.into()));
123        self
124    }
125
126    /// Execute a bash command
127    ///
128    /// # Arguments
129    ///
130    /// * `command` - Bash command to execute
131    ///
132    /// # Returns
133    ///
134    /// Bash execution result with stdout, stderr, and exit code
135    ///
136    /// # Example
137    ///
138    /// ```no_run
139    /// # use composio_sdk::meta_tools::BashExecutor;
140    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
141    /// let executor = BashExecutor::new();
142    /// let result = executor.execute("echo 'Hello, World!'").await?;
143    ///
144    /// println!("Output: {}", result.stdout);
145    /// println!("Exit code: {}", result.exit_code);
146    /// # Ok(())
147    /// # }
148    /// ```
149    pub async fn execute(&self, command: &str) -> Result<BashResult, ComposioError> {
150        // Ensure sandbox directory exists
151        if !self.sandbox_dir.exists() {
152            tokio::fs::create_dir_all(&self.sandbox_dir)
153                .await
154                .map_err(|e| ComposioError::ExecutionError(format!("Failed to create sandbox: {}", e)))?;
155        }
156
157        let start_time = std::time::Instant::now();
158
159        // Build command
160        let mut cmd = Command::new("bash");
161        cmd.arg("-c")
162            .arg(command)
163            .current_dir(&self.sandbox_dir)
164            .stdout(Stdio::piped())
165            .stderr(Stdio::piped());
166
167        // Add environment variables
168        for (key, value) in &self.env_vars {
169            cmd.env(key, value);
170        }
171
172        // Execute with timeout
173        let output = tokio::time::timeout(
174            std::time::Duration::from_secs(self.timeout_secs),
175            cmd.output(),
176        )
177        .await
178        .map_err(|_| {
179            ComposioError::ExecutionError(format!(
180                "Command timed out after {} seconds",
181                self.timeout_secs
182            ))
183        })?
184        .map_err(|e| ComposioError::ExecutionError(format!("Failed to execute command: {}", e)))?;
185
186        let execution_time_ms = start_time.elapsed().as_millis();
187
188        Ok(BashResult {
189            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
190            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
191            exit_code: output.status.code().unwrap_or(-1),
192            execution_time_ms,
193        })
194    }
195
196    /// Execute multiple commands sequentially
197    ///
198    /// # Arguments
199    ///
200    /// * `commands` - Vector of bash commands
201    ///
202    /// # Returns
203    ///
204    /// Vector of bash results (one per command)
205    ///
206    /// # Example
207    ///
208    /// ```no_run
209    /// # use composio_sdk::meta_tools::BashExecutor;
210    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
211    /// let executor = BashExecutor::new();
212    /// let results = executor.execute_batch(vec![
213    ///     "echo 'Step 1'",
214    ///     "echo 'Step 2'",
215    ///     "echo 'Step 3'",
216    /// ]).await?;
217    ///
218    /// for (i, result) in results.iter().enumerate() {
219    ///     println!("Command {}: {}", i + 1, result.stdout);
220    /// }
221    /// # Ok(())
222    /// # }
223    /// ```
224    pub async fn execute_batch(&self, commands: Vec<&str>) -> Result<Vec<BashResult>, ComposioError> {
225        let mut results = Vec::new();
226
227        for command in commands {
228            let result = self.execute(command).await?;
229            results.push(result);
230        }
231
232        Ok(results)
233    }
234
235    /// Get sandbox directory path
236    pub fn sandbox_dir(&self) -> &PathBuf {
237        &self.sandbox_dir
238    }
239}
240
241impl Default for BashExecutor {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[tokio::test]
252    async fn test_bash_executor_echo() {
253        let executor = BashExecutor::new();
254        let result = executor.execute("echo 'Hello, World!'").await.unwrap();
255
256        assert_eq!(result.exit_code, 0);
257        assert!(result.stdout.contains("Hello, World!"));
258        assert!(result.stderr.is_empty());
259    }
260
261    #[tokio::test]
262    async fn test_bash_executor_with_error() {
263        let executor = BashExecutor::new();
264        let result = executor.execute("ls /nonexistent_directory").await.unwrap();
265
266        assert_ne!(result.exit_code, 0);
267        assert!(!result.stderr.is_empty());
268    }
269
270    #[tokio::test]
271    async fn test_bash_executor_with_env() {
272        let executor = BashExecutor::new().env("TEST_VAR", "test_value");
273        let result = executor.execute("echo $TEST_VAR").await.unwrap();
274
275        assert_eq!(result.exit_code, 0);
276        assert!(result.stdout.contains("test_value"));
277    }
278
279    #[tokio::test]
280    async fn test_bash_executor_batch() {
281        let executor = BashExecutor::new();
282        let results = executor
283            .execute_batch(vec!["echo 'Step 1'", "echo 'Step 2'", "echo 'Step 3'"])
284            .await
285            .unwrap();
286
287        assert_eq!(results.len(), 3);
288        assert!(results[0].stdout.contains("Step 1"));
289        assert!(results[1].stdout.contains("Step 2"));
290        assert!(results[2].stdout.contains("Step 3"));
291    }
292
293    #[tokio::test]
294    async fn test_bash_executor_timeout() {
295        let executor = BashExecutor::new().timeout(1);
296        let result = executor.execute("sleep 5").await;
297
298        assert!(result.is_err());
299        assert!(result.unwrap_err().to_string().contains("timed out"));
300    }
301
302    #[test]
303    fn test_bash_result_clone() {
304        let result = BashResult {
305            stdout: "output".to_string(),
306            stderr: "error".to_string(),
307            exit_code: 0,
308            execution_time_ms: 100,
309        };
310
311        let cloned = result.clone();
312        assert_eq!(cloned.stdout, "output");
313        assert_eq!(cloned.exit_code, 0);
314    }
315}