adk_devtools/tools/
bash.rs1use std::process::Stdio;
13use std::sync::Arc;
14use std::time::Duration;
15
16use adk_core::{Result, Tool, ToolContext};
17use async_trait::async_trait;
18use serde_json::{Value, json};
19use tokio::io::{AsyncBufReadExt, BufReader};
20
21use crate::error::DevToolError;
22use crate::tools::read::require_str;
23use crate::workspace::Workspace;
24
25pub struct BashTool {
31 workspace: Workspace,
32}
33
34impl BashTool {
35 pub fn new(workspace: Workspace) -> Self {
37 Self { workspace }
38 }
39}
40
41#[async_trait]
42impl Tool for BashTool {
43 fn name(&self) -> &str {
44 "bash"
45 }
46
47 fn description(&self) -> &str {
48 "Run a shell command in the workspace root and return stdout, stderr, and the \
49 exit code. Streams output incrementally for live UI display. Has a timeout."
50 }
51
52 fn parameters_schema(&self) -> Option<Value> {
53 Some(json!({
54 "type": "object",
55 "properties": {
56 "command": { "type": "string", "description": "The shell command to run." },
57 "timeout_secs": { "type": "integer", "description": "Optional timeout in seconds (default: workspace setting)." }
58 },
59 "required": ["command"]
60 }))
61 }
62
63 async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
64 if !self.workspace.bash_allowed() {
65 return Err(DevToolError::BashDisabled.into());
66 }
67 let command = require_str(&args, "command")?;
68 let timeout = args
69 .get("timeout_secs")
70 .and_then(Value::as_u64)
71 .map(Duration::from_secs)
72 .unwrap_or_else(|| self.workspace.bash_timeout_value());
73
74 let mut cmd = tokio::process::Command::new("sh");
75 cmd.arg("-c")
76 .arg(&command)
77 .current_dir(self.workspace.root())
78 .stdout(Stdio::piped())
79 .stderr(Stdio::piped());
80
81 if !self.workspace.inherits_env() {
85 cmd.env_clear();
86 for (key, value) in self.workspace.bash_env() {
87 cmd.env(key, value);
88 }
89 }
90
91 #[cfg(unix)]
94 cmd.process_group(0);
95
96 let mut child = cmd.spawn().map_err(DevToolError::from)?;
97 let child_pid = child.id();
98
99 let stdout_pipe = child.stdout.take();
100 let stderr_pipe = child.stderr.take();
101 let cap = self.workspace.max_output();
102
103 let ctx_out = ctx.clone();
105 let stdout_task = tokio::spawn(async move {
106 let mut output = String::new();
107 if let Some(pipe) = stdout_pipe {
108 let mut reader = BufReader::new(pipe).lines();
109 while let Ok(Some(line)) = reader.next_line().await {
110 ctx_out.emit_progress("stdout", &format!("{line}\n")).await;
111 output.push_str(&line);
112 output.push('\n');
113 }
114 }
115 output
116 });
117
118 let ctx_err = ctx.clone();
119 let stderr_task = tokio::spawn(async move {
120 let mut output = String::new();
121 if let Some(pipe) = stderr_pipe {
122 let mut reader = BufReader::new(pipe).lines();
123 while let Ok(Some(line)) = reader.next_line().await {
124 ctx_err.emit_progress("stderr", &format!("{line}\n")).await;
125 output.push_str(&line);
126 output.push('\n');
127 }
128 }
129 output
130 });
131
132 let result = tokio::time::timeout(timeout, async {
134 let status = child.wait().await?;
135 let stdout = stdout_task.await.unwrap_or_default();
136 let stderr = stderr_task.await.unwrap_or_default();
137 Ok::<_, std::io::Error>((status, stdout, stderr))
138 })
139 .await;
140
141 match result {
142 Ok(Ok((status, stdout, stderr))) => {
143 let (stdout, out_trunc) = truncate(stdout, cap);
144 let (stderr, err_trunc) = truncate(stderr, cap);
145 Ok(json!({
146 "command": command,
147 "exit_code": status.code(),
148 "stdout": stdout,
149 "stderr": stderr,
150 "truncated": out_trunc || err_trunc,
151 }))
152 }
153 Ok(Err(e)) => Err(DevToolError::from(e).into()),
154 Err(_) => {
155 terminate_process_group(&mut child, child_pid);
156 ctx.emit_progress("stderr", &format!("\n[timeout after {}s]\n", timeout.as_secs()))
157 .await;
158 Err(DevToolError::Timeout(timeout).into())
159 }
160 }
161 }
162}
163
164fn terminate_process_group(child: &mut tokio::process::Child, pid: Option<u32>) {
170 #[cfg(unix)]
171 if let Some(pid) = pid {
172 unsafe {
175 libc::killpg(pid as libc::pid_t, libc::SIGKILL);
176 }
177 }
178 #[cfg(not(unix))]
179 let _ = pid;
180
181 let _ = child.start_kill();
183}
184
185fn truncate(mut s: String, cap: usize) -> (String, bool) {
186 if s.len() <= cap {
187 return (s, false);
188 }
189 s.truncate(cap);
190 s.push_str("\n…[truncated]");
191 (s, true)
192}