clawft_kernel/wasm_runner/
tools_fs.rs1use chrono::{DateTime, Utc};
4
5use super::catalog::builtin_tool_catalog;
6use super::registry::BuiltinTool;
7use super::types::*;
8
9const MAX_READ_SIZE: u64 = 8 * 1024 * 1024;
11
12pub struct FsReadFileTool {
18 spec: BuiltinToolSpec,
19 sandbox: SandboxConfig,
20}
21
22impl Default for FsReadFileTool {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl FsReadFileTool {
29 pub fn new() -> Self {
30 let catalog = builtin_tool_catalog();
31 let spec = catalog
32 .into_iter()
33 .find(|s| s.name == "fs.read_file")
34 .expect("fs.read_file must be in catalog");
35 Self {
36 spec,
37 sandbox: SandboxConfig::default(),
38 }
39 }
40
41 pub fn with_sandbox(sandbox: SandboxConfig) -> Self {
43 let catalog = builtin_tool_catalog();
44 let spec = catalog
45 .into_iter()
46 .find(|s| s.name == "fs.read_file")
47 .expect("fs.read_file must be in catalog");
48 Self { spec, sandbox }
49 }
50}
51
52impl BuiltinTool for FsReadFileTool {
53 fn name(&self) -> &str {
54 "fs.read_file"
55 }
56
57 fn spec(&self) -> &BuiltinToolSpec {
58 &self.spec
59 }
60
61 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
62 let path = args
63 .get("path")
64 .and_then(|v| v.as_str())
65 .ok_or_else(|| ToolError::InvalidArgs("missing 'path' parameter".into()))?;
66
67 let path = std::path::Path::new(path);
68
69 if !self.sandbox.is_path_allowed(path) {
71 return Err(ToolError::PermissionDenied(format!(
72 "path outside sandbox: {}",
73 path.display()
74 )));
75 }
76
77 if !path.exists() {
78 return Err(ToolError::FileNotFound(path.display().to_string()));
79 }
80
81 let metadata = std::fs::metadata(path)
82 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
83
84 if metadata.len() > MAX_READ_SIZE {
85 return Err(ToolError::FileTooLarge {
86 size: metadata.len(),
87 limit: MAX_READ_SIZE,
88 });
89 }
90
91 let offset = args
92 .get("offset")
93 .and_then(|v| v.as_u64())
94 .unwrap_or(0) as usize;
95 let limit = args
96 .get("limit")
97 .and_then(|v| v.as_u64())
98 .map(|v| v as usize);
99
100 let bytes = std::fs::read(path)
101 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
102
103 let end = match limit {
104 Some(l) => std::cmp::min(offset + l, bytes.len()),
105 None => bytes.len(),
106 };
107 let start = std::cmp::min(offset, bytes.len());
108 let slice = &bytes[start..end];
109
110 let content = String::from_utf8_lossy(slice).into_owned();
111 let modified = metadata
112 .modified()
113 .ok()
114 .map(|t| {
115 let dt: DateTime<Utc> = t.into();
116 dt.to_rfc3339()
117 })
118 .unwrap_or_default();
119
120 Ok(serde_json::json!({
121 "content": content,
122 "size": metadata.len(),
123 "modified": modified,
124 }))
125 }
126}
127
128pub struct FsWriteFileTool {
130 spec: BuiltinToolSpec,
131 sandbox: SandboxConfig,
132}
133
134impl FsWriteFileTool {
135 pub fn new() -> Self {
136 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.write_file").unwrap();
137 Self { spec, sandbox: SandboxConfig::default() }
138 }
139 pub fn with_sandbox(sandbox: SandboxConfig) -> Self {
140 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.write_file").unwrap();
141 Self { spec, sandbox }
142 }
143}
144
145impl BuiltinTool for FsWriteFileTool {
146 fn name(&self) -> &str { "fs.write_file" }
147 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
148 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
149 let path_str = args.get("path").and_then(|v| v.as_str())
150 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
151 let content = args.get("content").and_then(|v| v.as_str())
152 .ok_or_else(|| ToolError::InvalidArgs("missing 'content'".into()))?;
153 let append = args.get("append").and_then(|v| v.as_bool()).unwrap_or(false);
154 let path = std::path::Path::new(path_str);
155 if !self.sandbox.is_path_allowed(path) {
156 return Err(ToolError::PermissionDenied(format!("path outside sandbox: {}", path.display())));
157 }
158 if append {
159 use std::io::Write;
160 let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path)
161 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
162 f.write_all(content.as_bytes()).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
163 } else {
164 std::fs::write(path, content).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
165 }
166 Ok(serde_json::json!({"written": content.len(), "path": path_str}))
167 }
168}
169
170pub struct FsReadDirTool {
172 spec: BuiltinToolSpec,
173 sandbox: SandboxConfig,
174}
175
176impl FsReadDirTool {
177 pub fn new() -> Self {
178 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.read_dir").unwrap();
179 Self { spec, sandbox: SandboxConfig::default() }
180 }
181 pub fn with_sandbox(sandbox: SandboxConfig) -> Self {
182 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.read_dir").unwrap();
183 Self { spec, sandbox }
184 }
185}
186
187impl BuiltinTool for FsReadDirTool {
188 fn name(&self) -> &str { "fs.read_dir" }
189 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
190 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
191 let path_str = args.get("path").and_then(|v| v.as_str())
192 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
193 let path = std::path::Path::new(path_str);
194 if !self.sandbox.is_path_allowed(path) {
195 return Err(ToolError::PermissionDenied(format!("path outside sandbox: {}", path.display())));
196 }
197 if !path.exists() {
198 return Err(ToolError::FileNotFound(path.display().to_string()));
199 }
200 let entries: Vec<serde_json::Value> = std::fs::read_dir(path)
201 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?
202 .filter_map(|e| e.ok())
203 .map(|e| {
204 let ft = e.file_type().ok();
205 serde_json::json!({
206 "name": e.file_name().to_string_lossy(),
207 "is_dir": ft.as_ref().map(|t| t.is_dir()).unwrap_or(false),
208 "is_file": ft.as_ref().map(|t| t.is_file()).unwrap_or(false),
209 })
210 })
211 .collect();
212 Ok(serde_json::json!({"entries": entries, "count": entries.len()}))
213 }
214}
215
216pub struct FsCreateDirTool {
218 spec: BuiltinToolSpec,
219 sandbox: SandboxConfig,
220}
221
222impl FsCreateDirTool {
223 pub fn new() -> Self {
224 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.create_dir").unwrap();
225 Self { spec, sandbox: SandboxConfig::default() }
226 }
227}
228
229impl BuiltinTool for FsCreateDirTool {
230 fn name(&self) -> &str { "fs.create_dir" }
231 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
232 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
233 let path_str = args.get("path").and_then(|v| v.as_str())
234 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
235 let path = std::path::Path::new(path_str);
236 if !self.sandbox.is_path_allowed(path) {
237 return Err(ToolError::PermissionDenied(format!("path outside sandbox: {}", path.display())));
238 }
239 let recursive = args.get("recursive").and_then(|v| v.as_bool()).unwrap_or(true);
240 if recursive {
241 std::fs::create_dir_all(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
242 } else {
243 std::fs::create_dir(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
244 }
245 Ok(serde_json::json!({"created": path_str}))
246 }
247}
248
249pub struct FsRemoveTool {
251 spec: BuiltinToolSpec,
252 sandbox: SandboxConfig,
253}
254
255impl FsRemoveTool {
256 pub fn new() -> Self {
257 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.remove").unwrap();
258 Self { spec, sandbox: SandboxConfig::default() }
259 }
260}
261
262impl BuiltinTool for FsRemoveTool {
263 fn name(&self) -> &str { "fs.remove" }
264 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
265 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
266 let path_str = args.get("path").and_then(|v| v.as_str())
267 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
268 let path = std::path::Path::new(path_str);
269 if !self.sandbox.is_path_allowed(path) {
270 return Err(ToolError::PermissionDenied(format!("path outside sandbox: {}", path.display())));
271 }
272 if !path.exists() {
273 return Err(ToolError::FileNotFound(path.display().to_string()));
274 }
275 let recursive = args.get("recursive").and_then(|v| v.as_bool()).unwrap_or(false);
276 if path.is_dir() {
277 if recursive {
278 std::fs::remove_dir_all(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
279 } else {
280 std::fs::remove_dir(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
281 }
282 } else {
283 std::fs::remove_file(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
284 }
285 Ok(serde_json::json!({"removed": path_str}))
286 }
287}
288
289pub struct FsCopyTool {
291 spec: BuiltinToolSpec,
292 sandbox: SandboxConfig,
293}
294
295impl FsCopyTool {
296 pub fn new() -> Self {
297 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.copy").unwrap();
298 Self { spec, sandbox: SandboxConfig::default() }
299 }
300}
301
302impl BuiltinTool for FsCopyTool {
303 fn name(&self) -> &str { "fs.copy" }
304 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
305 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
306 let src_str = args.get("src").and_then(|v| v.as_str())
307 .ok_or_else(|| ToolError::InvalidArgs("missing 'src'".into()))?;
308 let dst_str = args.get("dst").and_then(|v| v.as_str())
309 .ok_or_else(|| ToolError::InvalidArgs("missing 'dst'".into()))?;
310 let src = std::path::Path::new(src_str);
311 let dst = std::path::Path::new(dst_str);
312 if !self.sandbox.is_path_allowed(src) || !self.sandbox.is_path_allowed(dst) {
313 return Err(ToolError::PermissionDenied("path outside sandbox".into()));
314 }
315 if !src.exists() {
316 return Err(ToolError::FileNotFound(src.display().to_string()));
317 }
318 let bytes = std::fs::copy(src, dst).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
319 Ok(serde_json::json!({"copied": bytes, "src": src_str, "dst": dst_str}))
320 }
321}
322
323pub struct FsMoveTool {
325 spec: BuiltinToolSpec,
326 sandbox: SandboxConfig,
327}
328
329impl FsMoveTool {
330 pub fn new() -> Self {
331 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.move").unwrap();
332 Self { spec, sandbox: SandboxConfig::default() }
333 }
334}
335
336impl BuiltinTool for FsMoveTool {
337 fn name(&self) -> &str { "fs.move" }
338 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
339 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
340 let src_str = args.get("src").and_then(|v| v.as_str())
341 .ok_or_else(|| ToolError::InvalidArgs("missing 'src'".into()))?;
342 let dst_str = args.get("dst").and_then(|v| v.as_str())
343 .ok_or_else(|| ToolError::InvalidArgs("missing 'dst'".into()))?;
344 let src = std::path::Path::new(src_str);
345 let dst = std::path::Path::new(dst_str);
346 if !self.sandbox.is_path_allowed(src) || !self.sandbox.is_path_allowed(dst) {
347 return Err(ToolError::PermissionDenied("path outside sandbox".into()));
348 }
349 if !src.exists() {
350 return Err(ToolError::FileNotFound(src.display().to_string()));
351 }
352 std::fs::rename(src, dst).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
353 Ok(serde_json::json!({"moved": true, "src": src_str, "dst": dst_str}))
354 }
355}
356
357pub struct FsStatTool {
359 spec: BuiltinToolSpec,
360 sandbox: SandboxConfig,
361}
362
363impl FsStatTool {
364 pub fn new() -> Self {
365 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.stat").unwrap();
366 Self { spec, sandbox: SandboxConfig::default() }
367 }
368}
369
370impl BuiltinTool for FsStatTool {
371 fn name(&self) -> &str { "fs.stat" }
372 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
373 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
374 let path_str = args.get("path").and_then(|v| v.as_str())
375 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
376 let path = std::path::Path::new(path_str);
377 if !self.sandbox.is_path_allowed(path) {
378 return Err(ToolError::PermissionDenied(format!("path outside sandbox: {}", path.display())));
379 }
380 let meta = std::fs::metadata(path)
381 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
382 let modified = meta.modified().ok().map(|t| {
383 let dt: DateTime<Utc> = t.into();
384 dt.to_rfc3339()
385 }).unwrap_or_default();
386 Ok(serde_json::json!({
387 "size": meta.len(),
388 "is_file": meta.is_file(),
389 "is_dir": meta.is_dir(),
390 "readonly": meta.permissions().readonly(),
391 "modified": modified,
392 }))
393 }
394}
395
396pub struct FsExistsTool {
398 spec: BuiltinToolSpec,
399}
400
401impl FsExistsTool {
402 pub fn new() -> Self {
403 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.exists").unwrap();
404 Self { spec }
405 }
406}
407
408impl BuiltinTool for FsExistsTool {
409 fn name(&self) -> &str { "fs.exists" }
410 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
411 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
412 let path_str = args.get("path").and_then(|v| v.as_str())
413 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
414 let path = std::path::Path::new(path_str);
415 let exists = path.exists();
416 let is_file = path.is_file();
417 let is_dir = path.is_dir();
418 Ok(serde_json::json!({"exists": exists, "is_file": is_file, "is_dir": is_dir}))
419 }
420}
421
422pub struct FsGlobTool {
424 spec: BuiltinToolSpec,
425 sandbox: SandboxConfig,
426}
427
428impl FsGlobTool {
429 pub fn new() -> Self {
430 let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "fs.glob").unwrap();
431 Self { spec, sandbox: SandboxConfig::default() }
432 }
433}
434
435impl BuiltinTool for FsGlobTool {
436 fn name(&self) -> &str { "fs.glob" }
437 fn spec(&self) -> &BuiltinToolSpec { &self.spec }
438 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
439 let pattern = args.get("pattern").and_then(|v| v.as_str())
440 .ok_or_else(|| ToolError::InvalidArgs("missing 'pattern'".into()))?;
441 let base_dir = args.get("base_dir").and_then(|v| v.as_str()).unwrap_or(".");
442 let base = std::path::Path::new(base_dir);
443 if !self.sandbox.is_path_allowed(base) {
444 return Err(ToolError::PermissionDenied("base_dir outside sandbox".into()));
445 }
446 let mut matches = Vec::new();
448 fn walk(dir: &std::path::Path, pattern: &str, matches: &mut Vec<String>) {
449 if let Ok(entries) = std::fs::read_dir(dir) {
450 for entry in entries.flatten() {
451 let path = entry.path();
452 let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
453 if simple_glob_match(pattern, &name) {
454 matches.push(path.display().to_string());
455 }
456 if path.is_dir() {
457 walk(&path, pattern, matches);
458 }
459 }
460 }
461 }
462 walk(base, pattern, &mut matches);
463 matches.sort();
464 Ok(serde_json::json!({"matches": matches, "count": matches.len()}))
465 }
466}
467
468pub(crate) fn simple_glob_match(pattern: &str, text: &str) -> bool {
470 let p: Vec<char> = pattern.chars().collect();
471 let t: Vec<char> = text.chars().collect();
472 simple_glob_match_inner(&p, &t, 0, 0)
473}
474
475fn simple_glob_match_inner(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
476 if pi == pattern.len() && ti == text.len() {
477 return true;
478 }
479 if pi == pattern.len() {
480 return false;
481 }
482 match pattern[pi] {
483 '*' => {
484 for i in ti..=text.len() {
486 if simple_glob_match_inner(pattern, text, pi + 1, i) {
487 return true;
488 }
489 }
490 false
491 }
492 '?' => {
493 if ti < text.len() {
494 simple_glob_match_inner(pattern, text, pi + 1, ti + 1)
495 } else {
496 false
497 }
498 }
499 c => {
500 if ti < text.len() && text[ti] == c {
501 simple_glob_match_inner(pattern, text, pi + 1, ti + 1)
502 } else {
503 false
504 }
505 }
506 }
507}