An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at main 237 lines 6.6 kB view raw
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}