mofa_plugins/tools/
filesystem.rs1use super::*;
2use serde_json::json;
3use tokio::fs;
4
5pub struct FileSystemTool {
7 definition: ToolDefinition,
8 allowed_paths: Vec<String>,
9}
10
11impl FileSystemTool {
12 pub fn new(allowed_paths: Vec<String>) -> Self {
13 Self {
14 definition: ToolDefinition {
15 name: "filesystem".to_string(),
16 description: "File system operations: read files, write files, list directories, check if file exists.".to_string(),
17 parameters: json!({
18 "type": "object",
19 "properties": {
20 "operation": {
21 "type": "string",
22 "enum": ["read", "write", "list", "exists", "delete", "mkdir"],
23 "description": "File operation to perform"
24 },
25 "path": {
26 "type": "string",
27 "description": "File or directory path"
28 },
29 "content": {
30 "type": "string",
31 "description": "Content to write (for write operation)"
32 }
33 },
34 "required": ["operation", "path"]
35 }),
36 requires_confirmation: true,
37 },
38 allowed_paths,
39 }
40 }
41
42 pub fn new_with_defaults() -> PluginResult<Self> {
44 Ok(Self::new(vec![
45 std::env::temp_dir().to_string_lossy().to_string(),
46 std::env::current_dir()?.to_string_lossy().to_string(),
47 ]))
48 }
49
50 fn is_path_allowed(&self, path: &str) -> bool {
51 if self.allowed_paths.is_empty() {
52 return true;
53 }
54 let path = std::path::Path::new(path);
55 self.allowed_paths.iter().any(|allowed| {
56 let allowed_path = std::path::Path::new(allowed);
57 path.starts_with(allowed_path)
58 })
59 }
60}
61
62#[async_trait::async_trait]
63impl ToolExecutor for FileSystemTool {
64 fn definition(&self) -> &ToolDefinition {
65 &self.definition
66 }
67
68 async fn execute(&self, arguments: serde_json::Value) -> PluginResult<serde_json::Value> {
69 let operation = arguments["operation"]
70 .as_str()
71 .ok_or_else(|| anyhow::anyhow!("Operation is required"))?;
72 let path = arguments["path"]
73 .as_str()
74 .ok_or_else(|| anyhow::anyhow!("Path is required"))?;
75
76 if !self.is_path_allowed(path) {
77 return Err(anyhow::anyhow!(
78 "Access denied: path '{}' is not in allowed paths",
79 path
80 ));
81 }
82
83 match operation {
84 "read" => {
85 let content = fs::read_to_string(path).await?;
86 let truncated = if content.len() > 10000 {
87 format!(
88 "{}... [truncated, total {} bytes]",
89 &content[..10000],
90 content.len()
91 )
92 } else {
93 content
94 };
95 Ok(json!({
96 "success": true,
97 "content": truncated
98 }))
99 }
100 "write" => {
101 let content = arguments["content"]
102 .as_str()
103 .ok_or_else(|| anyhow::anyhow!("Content is required for write operation"))?;
104 fs::write(path, content).await?;
105 Ok(json!({
106 "success": true,
107 "message": format!("Written {} bytes to {}", content.len(), path)
108 }))
109 }
110 "list" => {
111 let mut entries = Vec::new();
112 let mut dir = fs::read_dir(path).await?;
113 while let Some(entry) = dir.next_entry().await? {
114 let metadata = entry.metadata().await?;
115 entries.push(json!({
116 "name": entry.file_name().to_string_lossy(),
117 "is_dir": metadata.is_dir(),
118 "is_file": metadata.is_file(),
119 "size": metadata.len()
120 }));
121 }
122 Ok(json!({
123 "success": true,
124 "entries": entries
125 }))
126 }
127 "exists" => {
128 let exists = fs::try_exists(path).await?;
129 Ok(json!({
130 "success": true,
131 "exists": exists
132 }))
133 }
134 "delete" => {
135 let metadata = fs::metadata(path).await?;
136 if metadata.is_dir() {
137 fs::remove_dir_all(path).await?;
138 } else {
139 fs::remove_file(path).await?;
140 }
141 Ok(json!({
142 "success": true,
143 "message": format!("Deleted {}", path)
144 }))
145 }
146 "mkdir" => {
147 fs::create_dir_all(path).await?;
148 Ok(json!({
149 "success": true,
150 "message": format!("Created directory {}", path)
151 }))
152 }
153 _ => Err(anyhow::anyhow!("Unknown operation: {}", operation)),
154 }
155 }
156}