An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1use crate::security::permission::PermissionHandler;
2use crate::security::{SecurityValidator, ValidationResult};
3use anyhow::{Context, Result};
4use async_trait::async_trait;
5use serde_json::Value;
6use std::path::Path;
7use std::sync::Arc;
8use tokio::fs;
9
10use super::Tool;
11use super::permission_check::FilePermissionChecker;
12
13/// Tool for reading file contents
14pub struct ReadFileTool {
15 checker: FilePermissionChecker,
16 validator: Arc<SecurityValidator>,
17}
18
19impl ReadFileTool {
20 pub fn new(
21 validator: Arc<SecurityValidator>,
22 permission_handler: Arc<dyn PermissionHandler>,
23 ) -> Self {
24 Self {
25 checker: FilePermissionChecker::new(validator.clone(), permission_handler),
26 validator,
27 }
28 }
29}
30
31#[async_trait]
32impl Tool for ReadFileTool {
33 fn name(&self) -> &str {
34 "read_file"
35 }
36
37 fn description(&self) -> &str {
38 "Reads the contents of a file at the specified path"
39 }
40
41 fn parameters(&self) -> Value {
42 serde_json::json!({
43 "type": "object",
44 "properties": {
45 "path": {
46 "type": "string",
47 "description": "The path to the file to read"
48 }
49 },
50 "required": ["path"]
51 })
52 }
53
54 async fn execute(&self, params: Value) -> Result<String> {
55 let path = params["path"]
56 .as_str()
57 .context("Missing or invalid 'path' parameter")?;
58 let path = Path::new(path);
59
60 // Check permission
61 self.checker.check_permission(path)?;
62
63 // Check file size
64 match self.validator.check_file_size(path) {
65 ValidationResult::Allowed => {}
66 ValidationResult::Denied(reason) => {
67 anyhow::bail!("File too large: {}", reason);
68 }
69 ValidationResult::RequiresPermission(_) => {
70 // Shouldn't happen for size checks
71 }
72 }
73
74 let content = fs::read_to_string(path)
75 .await
76 .context(format!("Failed to read file: {}", path.display()))?;
77
78 Ok(content)
79 }
80}
81
82/// Tool for writing content to a file
83pub struct WriteFileTool {
84 checker: FilePermissionChecker,
85 validator: Arc<SecurityValidator>,
86}
87
88impl WriteFileTool {
89 pub fn new(
90 validator: Arc<SecurityValidator>,
91 permission_handler: Arc<dyn PermissionHandler>,
92 ) -> Self {
93 Self {
94 checker: FilePermissionChecker::new(validator.clone(), permission_handler),
95 validator,
96 }
97 }
98}
99
100#[async_trait]
101impl Tool for WriteFileTool {
102 fn name(&self) -> &str {
103 "write_file"
104 }
105
106 fn description(&self) -> &str {
107 "Writes content to a file at the specified path, creating parent directories if needed"
108 }
109
110 fn parameters(&self) -> Value {
111 serde_json::json!({
112 "type": "object",
113 "properties": {
114 "path": {
115 "type": "string",
116 "description": "The path to the file to write"
117 },
118 "content": {
119 "type": "string",
120 "description": "The content to write to the file"
121 }
122 },
123 "required": ["path", "content"]
124 })
125 }
126
127 async fn execute(&self, params: Value) -> Result<String> {
128 let path = params["path"]
129 .as_str()
130 .context("Missing or invalid 'path' parameter")?;
131 let content = params["content"]
132 .as_str()
133 .context("Missing or invalid 'content' parameter")?;
134 let path = Path::new(path);
135
136 // Check permission
137 self.checker.check_permission(path)?;
138
139 if let Some(parent) = path.parent() {
140 fs::create_dir_all(parent).await.context(format!(
141 "Failed to create parent directories for: {}",
142 path.display()
143 ))?;
144
145 let canonical_parent = parent.canonicalize().context(format!(
146 "Failed to resolve parent directory: {}",
147 parent.display()
148 ))?;
149 match self.validator.validate_file_path(&canonical_parent) {
150 ValidationResult::Allowed => {}
151 _ => anyhow::bail!(
152 "Parent directory resolved outside allowed paths: {}",
153 canonical_parent.display()
154 ),
155 }
156 }
157
158 fs::write(path, content)
159 .await
160 .context(format!("Failed to write file: {}", path.display()))?;
161
162 Ok(format!(
163 "Successfully wrote {} bytes to {}",
164 content.len(),
165 path.display()
166 ))
167 }
168}
169
170/// Tool for listing files in a directory
171pub struct ListFilesTool {
172 checker: FilePermissionChecker,
173}
174
175impl ListFilesTool {
176 pub fn new(
177 validator: Arc<SecurityValidator>,
178 permission_handler: Arc<dyn PermissionHandler>,
179 ) -> Self {
180 Self {
181 checker: FilePermissionChecker::new(validator, permission_handler),
182 }
183 }
184}
185
186#[async_trait]
187impl Tool for ListFilesTool {
188 fn name(&self) -> &str {
189 "list_files"
190 }
191
192 fn description(&self) -> &str {
193 "Lists files and directories at the specified path"
194 }
195
196 fn parameters(&self) -> Value {
197 serde_json::json!({
198 "type": "object",
199 "properties": {
200 "path": {
201 "type": "string",
202 "description": "The path to the directory to list"
203 }
204 },
205 "required": ["path"]
206 })
207 }
208
209 async fn execute(&self, params: Value) -> Result<String> {
210 let path = params["path"]
211 .as_str()
212 .context("Missing or invalid 'path' parameter")?;
213 let path = Path::new(path);
214
215 // Check permission
216 self.checker.check_permission(path)?;
217
218 let mut entries = fs::read_dir(path)
219 .await
220 .context(format!("Failed to read directory: {}", path.display()))?;
221
222 let mut items = Vec::new();
223 while let Some(entry) = entries.next_entry().await? {
224 let metadata = entry.metadata().await?;
225 let name = entry.file_name().to_string_lossy().to_string();
226
227 if metadata.is_dir() {
228 items.push(format!("{}/", name));
229 } else {
230 items.push(name);
231 }
232 }
233
234 items.sort();
235 Ok(items.join("\n"))
236 }
237}