1use super::*;
4
5impl BuiltinTools {
6 pub async fn execute(&self, name: &str, args: Value) -> String {
9 let canonical = canonical_tool_name(name);
10 if !self.available(canonical) {
13 return format!("[error] tool '{}' is not available on this platform", name);
14 }
15 match canonical {
16 "read_file" => self.read_file(&args).await,
17 "read_files" => self.read_files(&args).await,
18 "write_file" => self.write_file(&args).await,
19 "edit_file" => self.edit_file(&args).await,
20 "list_dir" => self.list_dir(&args).await,
21 "shell" => self.shell(&args).await,
22 n if n.starts_with("context_") => {
23 "[error] context tools must be handled by the runtime".to_string()
24 }
25 _ => format!("[error] Unknown built-in tool: {}", name),
26 }
27 }
28
29 pub(crate) fn ensure_workspace(&self) -> Result<(), String> {
38 if std::fs::metadata(&self.ctx.workdir).is_ok_and(|m| m.is_dir()) {
39 return Ok(());
40 }
41 Err(format!(
42 "[error] workspace '{}' is no longer accessible",
43 self.ctx.workdir.display()
44 ))
45 }
46
47 pub(crate) fn resolve(&self, requested: &str) -> anyhow::Result<PathBuf> {
73 Self::resolve_within(requested, &self.ctx.workdir, resolves_within)
74 }
75
76 pub(crate) fn resolve_within(
86 requested: &str,
87 workdir: &Path,
88 within: fn(&Path, &Path) -> bool,
89 ) -> anyhow::Result<PathBuf> {
90 let raw = if Path::new(requested).is_absolute() {
91 PathBuf::from(requested)
92 } else {
93 workdir.join(requested)
94 };
95
96 let mut normalized = PathBuf::new();
98 for component in raw.components() {
99 match component {
100 Component::ParentDir => {
101 if !normalized.pop() {
102 anyhow::bail!("path '{}' escapes the working directory", requested);
103 }
104 }
105 c => normalized.push(c),
106 }
107 }
108
109 if !normalized.starts_with(workdir) {
110 anyhow::bail!("path '{}' would escape the working directory", requested);
111 }
112
113 if !within(&normalized, workdir) {
114 anyhow::bail!(
115 "path '{requested}' resolves outside the working directory through a symlink"
116 );
117 }
118
119 Ok(normalized)
120 }
121
122 pub(crate) async fn read_file(&self, args: &Value) -> String {
123 let path_str = match args.get("path").and_then(|v| v.as_str()) {
124 Some(p) => p,
125 None => return "[error] missing 'path' argument".to_string(),
126 };
127
128 let path = match self.resolve(path_str) {
129 Ok(p) => p,
130 Err(e) => return format!("[error] {}", e),
131 };
132
133 match std::fs::read_to_string(&path) {
134 Ok(content) => content,
135 Err(e) => format!("[error] Failed to read '{}': {}", path_str, e),
136 }
137 }
138
139 pub(crate) async fn read_files(&self, args: &Value) -> String {
140 let paths = match args.get("paths").and_then(|v| v.as_array()) {
141 Some(arr) => arr,
142 None => return "[error] missing 'paths' argument (expected array)".to_string(),
143 };
144
145 if paths.is_empty() {
146 return "[error] 'paths' array is empty".to_string();
147 }
148
149 let mut results = Vec::with_capacity(paths.len());
150 for path_val in paths {
151 let path_str = match path_val.as_str() {
152 Some(p) => p,
153 None => {
154 results.push("[error] non-string path in array".to_string());
155 continue;
156 }
157 };
158
159 let path = match self.resolve(path_str) {
160 Ok(p) => p,
161 Err(e) => {
162 results.push(format!("### [{}]\n[error] {}", path_str, e));
163 continue;
164 }
165 };
166
167 match std::fs::read_to_string(&path) {
168 Ok(content) => {
169 results.push(format!("### [{}]\n{}", path_str, content));
170 }
171 Err(e) => {
172 results.push(format!("### [{}]\n[error] Failed to read: {}", path_str, e));
173 }
174 }
175 }
176
177 results.join("\n\n")
178 }
179
180 pub(crate) async fn write_file(&self, args: &Value) -> String {
181 let path_str = match args.get("path").and_then(|v| v.as_str()) {
182 Some(p) => p,
183 None => return "[error] missing 'path' argument".to_string(),
184 };
185 let content = match args.get("content").and_then(|v| v.as_str()) {
186 Some(c) => c,
187 None => return "[error] missing 'content' argument".to_string(),
188 };
189 if let Err(e) = self.ensure_workspace() {
190 return e;
191 }
192
193 let path = match self.resolve(path_str) {
194 Ok(p) => p,
195 Err(e) => return format!("[error] {}", e),
196 };
197
198 let lock = self.ctx.lock_for(&path);
200 let _guard = lock.lock().await;
201
202 let parent = {
203 let mut p = path.clone();
204 p.pop();
205 p
206 };
207 if let Err(e) = std::fs::create_dir_all(&parent) {
208 return format!(
209 "[error] Failed to create directories for '{}': {}",
210 path_str, e
211 );
212 }
213
214 match std::fs::write(&path, content) {
215 Ok(()) => format!(
216 "Successfully wrote {} bytes to '{}'",
217 content.len(),
218 path_str
219 ),
220 Err(e) => format!("[error] Failed to write '{}': {}", path_str, e),
221 }
222 }
223
224 pub(crate) async fn edit_file(&self, args: &Value) -> String {
225 let path_str = match args.get("path").and_then(|v| v.as_str()) {
226 Some(p) => p,
227 None => return "[error] missing 'path' argument".to_string(),
228 };
229 let old_str = match args.get("old_str").and_then(|v| v.as_str()) {
230 Some(s) => s,
231 None => return "[error] missing 'old_str' argument".to_string(),
232 };
233 let new_str = match args.get("new_str").and_then(|v| v.as_str()) {
234 Some(s) => s,
235 None => return "[error] missing 'new_str' argument".to_string(),
236 };
237 if let Err(e) = self.ensure_workspace() {
238 return e;
239 }
240
241 let path = match self.resolve(path_str) {
242 Ok(p) => p,
243 Err(e) => return format!("[error] {}", e),
244 };
245
246 let lock = self.ctx.lock_for(&path);
249 let _guard = lock.lock().await;
250
251 let content = match std::fs::read_to_string(&path) {
252 Ok(c) => c,
253 Err(e) => return format!("[error] Failed to read '{}': {}", path_str, e),
254 };
255
256 let count = content.matches(old_str).count();
257 match count {
258 0 => format!(
259 "[error] String not found in '{}'. Ensure old_str matches the file exactly.",
260 path_str
261 ),
262 1 => {
263 let new_content = content.replacen(old_str, new_str, 1);
264 match std::fs::write(&path, &new_content) {
265 Ok(()) => format!("Successfully edited '{}'", path_str),
266 Err(e) => format!("[error] Failed to write '{}': {}", path_str, e),
267 }
268 }
269 n => format!(
270 "[error] Found {} occurrences of the string in '{}'. old_str must be unique.",
271 n, path_str
272 ),
273 }
274 }
275
276 pub(crate) async fn list_dir(&self, args: &Value) -> String {
277 let path_str = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
278
279 let path = match self.resolve(path_str) {
280 Ok(p) => p,
281 Err(e) => return format!("[error] {}", e),
282 };
283
284 let entries = match std::fs::read_dir(&path) {
285 Ok(e) => e,
286 Err(e) => return format!("[error] Failed to read directory '{}': {}", path_str, e),
287 };
288
289 let mut items: Vec<_> = entries.filter_map(|e| e.ok()).collect();
290 items.sort_by_key(|e| e.file_name());
291
292 let mut lines = Vec::new();
293 for entry in items {
294 let name = entry.file_name().to_string_lossy().to_string();
295 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
296 if is_dir {
297 lines.push(format!("{}/", name));
298 } else {
299 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
300 lines.push(format!("{} ({}B)", name, size));
301 }
302 }
303
304 if lines.is_empty() {
305 format!("(empty directory: {})", path_str)
306 } else {
307 lines.join("\n")
308 }
309 }
310
311 pub(crate) fn detect_shell() -> (&'static str, &'static str) {
317 #[cfg(windows)]
318 {
319 ("cmd.exe", "/C")
320 }
321
322 #[cfg(not(windows))]
323 {
324 Self::detect_shell_impl(std::env::var("SHELL").ok(), &|s| {
325 std::path::Path::new(s).exists()
326 })
327 }
328 }
329
330 #[cfg(not(windows))]
341 pub(crate) fn detect_shell_impl(
342 env_shell: Option<String>,
343 shell_exists: &dyn Fn(&str) -> bool,
344 ) -> (&'static str, &'static str) {
345 if let Some(shell) = env_shell
346 && (shell.ends_with("/zsh") || shell.ends_with("/bash") || shell.ends_with("/sh"))
347 && shell_exists(&shell)
348 {
349 let shell: &'static str = Box::leak(shell.into_boxed_str());
354 return (shell, "-c");
355 }
356 for &shell in &[
357 "/bin/bash",
358 "/usr/bin/bash",
359 "/bin/zsh",
360 "/usr/bin/zsh",
361 "/bin/sh",
362 ] {
363 if shell_exists(shell) {
364 return (shell, "-c");
365 }
366 }
367 ("sh", "-c")
368 }
369
370 pub(crate) async fn shell(&self, args: &Value) -> String {
371 self.shell_with_timeout(args, Duration::from_secs(60)).await
372 }
373
374 pub(crate) async fn shell_with_timeout(
377 &self,
378 args: &Value,
379 timeout_duration: Duration,
380 ) -> String {
381 let command = match args.get("command").and_then(|v| v.as_str()) {
382 Some(c) => c,
383 None => return "[error] missing 'command' argument".to_string(),
384 };
385
386 let workdir = self.ctx.workdir.clone();
387 let (shell, flag) = Self::detect_shell();
388
389 let mut cmd = match &self.shell_executor {
393 Some(executor) => executor.build_command(shell, flag, command, &workdir),
394 None => {
395 let mut c = Command::new(shell);
396 c.arg(flag).arg(command).current_dir(&workdir);
397 c
398 }
399 };
400 cmd.kill_on_drop(true);
411 own_process_group(&mut cmd);
412 cmd.stdout(std::process::Stdio::piped())
415 .stderr(std::process::Stdio::piped());
416
417 let run = async {
423 let child = cmd.spawn()?;
424 let _reaper = child.id().map(ProcessGroupReaper);
426 child.wait_with_output().await
427 };
428
429 match timeout(timeout_duration, run).await {
430 Err(_) => format!("[timed out] Command exceeded 60s: {}", command),
431 Ok(Err(e)) => format!("[error] Failed to spawn shell '{}': {}", shell, e),
432 Ok(Ok(output)) => Self::format_command_output(
433 &output.stdout,
434 &output.stderr,
435 output.status.success(),
436 output.status.code().unwrap_or(-1),
437 ),
438 }
439 }
440
441 pub(crate) fn format_command_output(
449 stdout: &[u8],
450 stderr: &[u8],
451 success: bool,
452 exit_code: i32,
453 ) -> String {
454 let stdout = String::from_utf8_lossy(stdout);
455 let stderr = String::from_utf8_lossy(stderr);
456
457 if success {
458 if stdout.trim().is_empty() {
459 "(command succeeded with no output)".to_string()
460 } else {
461 stdout.to_string()
462 }
463 } else {
464 let mut result = format!("[exit code {}]\n", exit_code);
465 if !stdout.trim().is_empty() {
466 result.push_str(&format!("stdout:\n{}\n", stdout));
467 }
468 if !stderr.trim().is_empty() {
469 result.push_str(&format!("stderr:\n{}", stderr));
470 }
471 result
472 }
473 }
474}