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 =
82 std::fs::metadata(path).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.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
92 let limit = args
93 .get("limit")
94 .and_then(|v| v.as_u64())
95 .map(|v| v as usize);
96
97 let bytes = std::fs::read(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
98
99 let end = match limit {
100 Some(l) => std::cmp::min(offset + l, bytes.len()),
101 None => bytes.len(),
102 };
103 let start = std::cmp::min(offset, bytes.len());
104 let slice = &bytes[start..end];
105
106 let content = String::from_utf8_lossy(slice).into_owned();
107 let modified = metadata
108 .modified()
109 .ok()
110 .map(|t| {
111 let dt: DateTime<Utc> = t.into();
112 dt.to_rfc3339()
113 })
114 .unwrap_or_default();
115
116 Ok(serde_json::json!({
117 "content": content,
118 "size": metadata.len(),
119 "modified": modified,
120 }))
121 }
122}
123
124pub struct FsWriteFileTool {
126 spec: BuiltinToolSpec,
127 sandbox: SandboxConfig,
128}
129
130impl FsWriteFileTool {
131 pub fn new() -> Self {
132 let spec = builtin_tool_catalog()
133 .into_iter()
134 .find(|s| s.name == "fs.write_file")
135 .unwrap();
136 Self {
137 spec,
138 sandbox: SandboxConfig::default(),
139 }
140 }
141 pub fn with_sandbox(sandbox: SandboxConfig) -> Self {
142 let spec = builtin_tool_catalog()
143 .into_iter()
144 .find(|s| s.name == "fs.write_file")
145 .unwrap();
146 Self { spec, sandbox }
147 }
148}
149
150impl BuiltinTool for FsWriteFileTool {
151 fn name(&self) -> &str {
152 "fs.write_file"
153 }
154 fn spec(&self) -> &BuiltinToolSpec {
155 &self.spec
156 }
157 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
158 let path_str = args
159 .get("path")
160 .and_then(|v| v.as_str())
161 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
162 let content = args
163 .get("content")
164 .and_then(|v| v.as_str())
165 .ok_or_else(|| ToolError::InvalidArgs("missing 'content'".into()))?;
166 let append = args
167 .get("append")
168 .and_then(|v| v.as_bool())
169 .unwrap_or(false);
170 let path = std::path::Path::new(path_str);
171 if !self.sandbox.is_path_allowed(path) {
172 return Err(ToolError::PermissionDenied(format!(
173 "path outside sandbox: {}",
174 path.display()
175 )));
176 }
177 if append {
178 use std::io::Write;
179 let mut f = std::fs::OpenOptions::new()
180 .create(true)
181 .append(true)
182 .open(path)
183 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
184 f.write_all(content.as_bytes())
185 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
186 } else {
187 std::fs::write(path, content).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
188 }
189
190 #[cfg(feature = "exochain")]
192 clawft_core::chain_event::push_chain_event(
193 "wasm_fs",
194 "wasm.fs.write",
195 Some(
196 serde_json::json!({"path": path_str, "bytes_written": content.len(), "append": append}),
197 ),
198 );
199
200 Ok(serde_json::json!({"written": content.len(), "path": path_str}))
201 }
202}
203
204pub struct FsReadDirTool {
206 spec: BuiltinToolSpec,
207 sandbox: SandboxConfig,
208}
209
210impl FsReadDirTool {
211 pub fn new() -> Self {
212 let spec = builtin_tool_catalog()
213 .into_iter()
214 .find(|s| s.name == "fs.read_dir")
215 .unwrap();
216 Self {
217 spec,
218 sandbox: SandboxConfig::default(),
219 }
220 }
221 pub fn with_sandbox(sandbox: SandboxConfig) -> Self {
222 let spec = builtin_tool_catalog()
223 .into_iter()
224 .find(|s| s.name == "fs.read_dir")
225 .unwrap();
226 Self { spec, sandbox }
227 }
228}
229
230impl BuiltinTool for FsReadDirTool {
231 fn name(&self) -> &str {
232 "fs.read_dir"
233 }
234 fn spec(&self) -> &BuiltinToolSpec {
235 &self.spec
236 }
237 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
238 let path_str = args
239 .get("path")
240 .and_then(|v| v.as_str())
241 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
242 let path = std::path::Path::new(path_str);
243 if !self.sandbox.is_path_allowed(path) {
244 return Err(ToolError::PermissionDenied(format!(
245 "path outside sandbox: {}",
246 path.display()
247 )));
248 }
249 if !path.exists() {
250 return Err(ToolError::FileNotFound(path.display().to_string()));
251 }
252 let entries: Vec<serde_json::Value> = std::fs::read_dir(path)
253 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?
254 .filter_map(|e| e.ok())
255 .map(|e| {
256 let ft = e.file_type().ok();
257 serde_json::json!({
258 "name": e.file_name().to_string_lossy(),
259 "is_dir": ft.as_ref().map(|t| t.is_dir()).unwrap_or(false),
260 "is_file": ft.as_ref().map(|t| t.is_file()).unwrap_or(false),
261 })
262 })
263 .collect();
264 Ok(serde_json::json!({"entries": entries, "count": entries.len()}))
265 }
266}
267
268pub struct FsCreateDirTool {
270 spec: BuiltinToolSpec,
271 sandbox: SandboxConfig,
272}
273
274impl FsCreateDirTool {
275 pub fn new() -> Self {
276 let spec = builtin_tool_catalog()
277 .into_iter()
278 .find(|s| s.name == "fs.create_dir")
279 .unwrap();
280 Self {
281 spec,
282 sandbox: SandboxConfig::default(),
283 }
284 }
285}
286
287impl BuiltinTool for FsCreateDirTool {
288 fn name(&self) -> &str {
289 "fs.create_dir"
290 }
291 fn spec(&self) -> &BuiltinToolSpec {
292 &self.spec
293 }
294 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
295 let path_str = args
296 .get("path")
297 .and_then(|v| v.as_str())
298 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
299 let path = std::path::Path::new(path_str);
300 if !self.sandbox.is_path_allowed(path) {
301 return Err(ToolError::PermissionDenied(format!(
302 "path outside sandbox: {}",
303 path.display()
304 )));
305 }
306 let recursive = args
307 .get("recursive")
308 .and_then(|v| v.as_bool())
309 .unwrap_or(true);
310 if recursive {
311 std::fs::create_dir_all(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
312 } else {
313 std::fs::create_dir(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
314 }
315
316 #[cfg(feature = "exochain")]
318 clawft_core::chain_event::push_chain_event(
319 "wasm_fs",
320 "wasm.fs.create_dir",
321 Some(serde_json::json!({"path": path_str, "recursive": recursive})),
322 );
323
324 Ok(serde_json::json!({"created": path_str}))
325 }
326}
327
328pub struct FsRemoveTool {
330 spec: BuiltinToolSpec,
331 sandbox: SandboxConfig,
332}
333
334impl FsRemoveTool {
335 pub fn new() -> Self {
336 let spec = builtin_tool_catalog()
337 .into_iter()
338 .find(|s| s.name == "fs.remove")
339 .unwrap();
340 Self {
341 spec,
342 sandbox: SandboxConfig::default(),
343 }
344 }
345}
346
347impl BuiltinTool for FsRemoveTool {
348 fn name(&self) -> &str {
349 "fs.remove"
350 }
351 fn spec(&self) -> &BuiltinToolSpec {
352 &self.spec
353 }
354 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
355 let path_str = args
356 .get("path")
357 .and_then(|v| v.as_str())
358 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
359 let path = std::path::Path::new(path_str);
360 if !self.sandbox.is_path_allowed(path) {
361 return Err(ToolError::PermissionDenied(format!(
362 "path outside sandbox: {}",
363 path.display()
364 )));
365 }
366 if !path.exists() {
367 return Err(ToolError::FileNotFound(path.display().to_string()));
368 }
369 let recursive = args
370 .get("recursive")
371 .and_then(|v| v.as_bool())
372 .unwrap_or(false);
373 if path.is_dir() {
374 if recursive {
375 std::fs::remove_dir_all(path)
376 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
377 } else {
378 std::fs::remove_dir(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
379 }
380 } else {
381 std::fs::remove_file(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
382 }
383
384 #[cfg(feature = "exochain")]
386 clawft_core::chain_event::push_chain_event(
387 "wasm_fs",
388 "wasm.fs.remove",
389 Some(serde_json::json!({"path": path_str, "recursive": recursive})),
390 );
391
392 Ok(serde_json::json!({"removed": path_str}))
393 }
394}
395
396pub struct FsCopyTool {
398 spec: BuiltinToolSpec,
399 sandbox: SandboxConfig,
400}
401
402impl FsCopyTool {
403 pub fn new() -> Self {
404 let spec = builtin_tool_catalog()
405 .into_iter()
406 .find(|s| s.name == "fs.copy")
407 .unwrap();
408 Self {
409 spec,
410 sandbox: SandboxConfig::default(),
411 }
412 }
413}
414
415impl BuiltinTool for FsCopyTool {
416 fn name(&self) -> &str {
417 "fs.copy"
418 }
419 fn spec(&self) -> &BuiltinToolSpec {
420 &self.spec
421 }
422 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
423 let src_str = args
424 .get("src")
425 .and_then(|v| v.as_str())
426 .ok_or_else(|| ToolError::InvalidArgs("missing 'src'".into()))?;
427 let dst_str = args
428 .get("dst")
429 .and_then(|v| v.as_str())
430 .ok_or_else(|| ToolError::InvalidArgs("missing 'dst'".into()))?;
431 let src = std::path::Path::new(src_str);
432 let dst = std::path::Path::new(dst_str);
433 if !self.sandbox.is_path_allowed(src) || !self.sandbox.is_path_allowed(dst) {
434 return Err(ToolError::PermissionDenied("path outside sandbox".into()));
435 }
436 if !src.exists() {
437 return Err(ToolError::FileNotFound(src.display().to_string()));
438 }
439 let bytes =
440 std::fs::copy(src, dst).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
441
442 #[cfg(feature = "exochain")]
444 clawft_core::chain_event::push_chain_event(
445 "wasm_fs",
446 "wasm.fs.copy",
447 Some(serde_json::json!({"src": src_str, "dst": dst_str, "bytes_copied": bytes})),
448 );
449
450 Ok(serde_json::json!({"copied": bytes, "src": src_str, "dst": dst_str}))
451 }
452}
453
454pub struct FsMoveTool {
456 spec: BuiltinToolSpec,
457 sandbox: SandboxConfig,
458}
459
460impl FsMoveTool {
461 pub fn new() -> Self {
462 let spec = builtin_tool_catalog()
463 .into_iter()
464 .find(|s| s.name == "fs.move")
465 .unwrap();
466 Self {
467 spec,
468 sandbox: SandboxConfig::default(),
469 }
470 }
471}
472
473impl BuiltinTool for FsMoveTool {
474 fn name(&self) -> &str {
475 "fs.move"
476 }
477 fn spec(&self) -> &BuiltinToolSpec {
478 &self.spec
479 }
480 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
481 let src_str = args
482 .get("src")
483 .and_then(|v| v.as_str())
484 .ok_or_else(|| ToolError::InvalidArgs("missing 'src'".into()))?;
485 let dst_str = args
486 .get("dst")
487 .and_then(|v| v.as_str())
488 .ok_or_else(|| ToolError::InvalidArgs("missing 'dst'".into()))?;
489 let src = std::path::Path::new(src_str);
490 let dst = std::path::Path::new(dst_str);
491 if !self.sandbox.is_path_allowed(src) || !self.sandbox.is_path_allowed(dst) {
492 return Err(ToolError::PermissionDenied("path outside sandbox".into()));
493 }
494 if !src.exists() {
495 return Err(ToolError::FileNotFound(src.display().to_string()));
496 }
497 std::fs::rename(src, dst).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
498
499 #[cfg(feature = "exochain")]
501 clawft_core::chain_event::push_chain_event(
502 "wasm_fs",
503 "wasm.fs.move",
504 Some(serde_json::json!({"src": src_str, "dst": dst_str})),
505 );
506
507 Ok(serde_json::json!({"moved": true, "src": src_str, "dst": dst_str}))
508 }
509}
510
511pub struct FsStatTool {
513 spec: BuiltinToolSpec,
514 sandbox: SandboxConfig,
515}
516
517impl FsStatTool {
518 pub fn new() -> Self {
519 let spec = builtin_tool_catalog()
520 .into_iter()
521 .find(|s| s.name == "fs.stat")
522 .unwrap();
523 Self {
524 spec,
525 sandbox: SandboxConfig::default(),
526 }
527 }
528}
529
530impl BuiltinTool for FsStatTool {
531 fn name(&self) -> &str {
532 "fs.stat"
533 }
534 fn spec(&self) -> &BuiltinToolSpec {
535 &self.spec
536 }
537 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
538 let path_str = args
539 .get("path")
540 .and_then(|v| v.as_str())
541 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
542 let path = std::path::Path::new(path_str);
543 if !self.sandbox.is_path_allowed(path) {
544 return Err(ToolError::PermissionDenied(format!(
545 "path outside sandbox: {}",
546 path.display()
547 )));
548 }
549 let meta =
550 std::fs::metadata(path).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
551 let modified = meta
552 .modified()
553 .ok()
554 .map(|t| {
555 let dt: DateTime<Utc> = t.into();
556 dt.to_rfc3339()
557 })
558 .unwrap_or_default();
559 Ok(serde_json::json!({
560 "size": meta.len(),
561 "is_file": meta.is_file(),
562 "is_dir": meta.is_dir(),
563 "readonly": meta.permissions().readonly(),
564 "modified": modified,
565 }))
566 }
567}
568
569pub struct FsExistsTool {
571 spec: BuiltinToolSpec,
572}
573
574impl FsExistsTool {
575 pub fn new() -> Self {
576 let spec = builtin_tool_catalog()
577 .into_iter()
578 .find(|s| s.name == "fs.exists")
579 .unwrap();
580 Self { spec }
581 }
582}
583
584impl BuiltinTool for FsExistsTool {
585 fn name(&self) -> &str {
586 "fs.exists"
587 }
588 fn spec(&self) -> &BuiltinToolSpec {
589 &self.spec
590 }
591 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
592 let path_str = args
593 .get("path")
594 .and_then(|v| v.as_str())
595 .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
596 let path = std::path::Path::new(path_str);
597 let exists = path.exists();
598 let is_file = path.is_file();
599 let is_dir = path.is_dir();
600 Ok(serde_json::json!({"exists": exists, "is_file": is_file, "is_dir": is_dir}))
601 }
602}
603
604pub struct FsGlobTool {
606 spec: BuiltinToolSpec,
607 sandbox: SandboxConfig,
608}
609
610impl FsGlobTool {
611 pub fn new() -> Self {
612 let spec = builtin_tool_catalog()
613 .into_iter()
614 .find(|s| s.name == "fs.glob")
615 .unwrap();
616 Self {
617 spec,
618 sandbox: SandboxConfig::default(),
619 }
620 }
621}
622
623impl BuiltinTool for FsGlobTool {
624 fn name(&self) -> &str {
625 "fs.glob"
626 }
627 fn spec(&self) -> &BuiltinToolSpec {
628 &self.spec
629 }
630 fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
631 let pattern = args
632 .get("pattern")
633 .and_then(|v| v.as_str())
634 .ok_or_else(|| ToolError::InvalidArgs("missing 'pattern'".into()))?;
635 let base_dir = args.get("base_dir").and_then(|v| v.as_str()).unwrap_or(".");
636 let base = std::path::Path::new(base_dir);
637 if !self.sandbox.is_path_allowed(base) {
638 return Err(ToolError::PermissionDenied(
639 "base_dir outside sandbox".into(),
640 ));
641 }
642 let mut matches = Vec::new();
644 fn walk(dir: &std::path::Path, pattern: &str, matches: &mut Vec<String>) {
645 if let Ok(entries) = std::fs::read_dir(dir) {
646 for entry in entries.flatten() {
647 let path = entry.path();
648 let name = path
649 .file_name()
650 .map(|n| n.to_string_lossy().to_string())
651 .unwrap_or_default();
652 if simple_glob_match(pattern, &name) {
653 matches.push(path.display().to_string());
654 }
655 if path.is_dir() {
656 walk(&path, pattern, matches);
657 }
658 }
659 }
660 }
661 walk(base, pattern, &mut matches);
662 matches.sort();
663 Ok(serde_json::json!({"matches": matches, "count": matches.len()}))
664 }
665}
666
667pub(crate) fn simple_glob_match(pattern: &str, text: &str) -> bool {
669 let p: Vec<char> = pattern.chars().collect();
670 let t: Vec<char> = text.chars().collect();
671 simple_glob_match_inner(&p, &t, 0, 0)
672}
673
674fn simple_glob_match_inner(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool {
675 if pi == pattern.len() && ti == text.len() {
676 return true;
677 }
678 if pi == pattern.len() {
679 return false;
680 }
681 match pattern[pi] {
682 '*' => {
683 for i in ti..=text.len() {
685 if simple_glob_match_inner(pattern, text, pi + 1, i) {
686 return true;
687 }
688 }
689 false
690 }
691 '?' => {
692 if ti < text.len() {
693 simple_glob_match_inner(pattern, text, pi + 1, ti + 1)
694 } else {
695 false
696 }
697 }
698 c => {
699 if ti < text.len() && text[ti] == c {
700 simple_glob_match_inner(pattern, text, pi + 1, ti + 1)
701 } else {
702 false
703 }
704 }
705 }
706}