claude_rust_tools/infrastructure/
bash_tool.rs1use claude_rust_errors::{AppError, AppResult};
2use claude_rust_types::{InterruptBehavior, PermissionLevel, Tool};
3use serde_json::{Value, json};
4use tokio::process::Command;
5
6pub struct BashTool;
7
8#[async_trait::async_trait]
9impl Tool for BashTool {
10 fn name(&self) -> &str {
11 "bash"
12 }
13
14 fn description(&self) -> &str {
15 "Execute a bash command and return its output."
16 }
17
18 fn input_schema(&self) -> Value {
19 json!({
20 "type": "object",
21 "properties": {
22 "command": {
23 "type": "string",
24 "description": "The bash command to execute"
25 }
26 },
27 "required": ["command"]
28 })
29 }
30
31 fn permission_level(&self) -> PermissionLevel {
32 PermissionLevel::Dangerous
33 }
34
35 fn interrupt_behavior(&self) -> InterruptBehavior {
36 InterruptBehavior::Cancel
37 }
38
39 async fn execute(&self, input: Value) -> AppResult<String> {
40 let command = input
41 .get("command")
42 .and_then(|v| v.as_str())
43 .ok_or_else(|| AppError::Tool("missing 'command' field".into()))?;
44
45 tracing::info!(command, "executing bash");
46
47 let output = Command::new("bash")
48 .arg("-c")
49 .arg(command)
50 .output()
51 .await
52 .map_err(|e| AppError::Tool(format!("failed to spawn bash: {e}")))?;
53
54 let stdout = String::from_utf8_lossy(&output.stdout);
55 let stderr = String::from_utf8_lossy(&output.stderr);
56
57 let mut result = String::new();
58 if !stdout.is_empty() {
59 result.push_str(&stdout);
60 }
61 if !stderr.is_empty() {
62 if !result.is_empty() {
63 result.push('\n');
64 }
65 result.push_str("STDERR:\n");
66 result.push_str(&stderr);
67 }
68 if result.is_empty() {
69 result.push_str("(no output)");
70 }
71
72 if result.len() > 100_000 {
73 result.truncate(100_000);
74 result.push_str("\n... (truncated)");
75 }
76
77 Ok(result)
78 }
79}