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}