//! File read tool with path validation use async_trait::async_trait; use serde_json::{json, Value}; use zclaw_types::{Result, ZclawError}; use std::fs; use std::io::Read; use crate::tool::{Tool, ToolContext}; use super::path_validator::PathValidator; pub struct FileReadTool; impl FileReadTool { pub fn new() -> Self { Self } } #[async_trait] impl Tool for FileReadTool { fn name(&self) -> &str { "file_read" } fn description(&self) -> &str { "Read the contents of a file from the filesystem. The file must be within allowed paths." } fn input_schema(&self) -> Value { json!({ "type": "object", "properties": { "path": { "type": "string", "description": "The path to the file to read" }, "encoding": { "type": "string", "description": "Text encoding to use (default: utf-8)", "enum": ["utf-8", "ascii", "binary"] } }, "required": ["path"] }) } async fn execute(&self, input: Value, context: &ToolContext) -> Result { let path = input["path"].as_str() .ok_or_else(|| ZclawError::InvalidInput("Missing 'path' parameter".into()))?; let encoding = input["encoding"].as_str().unwrap_or("utf-8"); // Validate path using context's path validator or create default let validator = context.path_validator.as_ref() .map(|v| v.clone()) .unwrap_or_else(|| { // Create default validator with workspace as allowed path let mut validator = PathValidator::new(); if let Some(ref workspace) = context.working_directory { validator = validator.with_workspace(std::path::PathBuf::from(workspace)); } validator }); // Validate path for read access let validated_path = validator.validate_read(path)?; // Read file content let mut file = fs::File::open(&validated_path) .map_err(|e| ZclawError::ToolError(format!("Failed to open file: {}", e)))?; let metadata = fs::metadata(&validated_path) .map_err(|e| ZclawError::ToolError(format!("Failed to read file metadata: {}", e)))?; let file_size = metadata.len(); match encoding { "binary" => { let mut buffer = Vec::with_capacity(file_size as usize); file.read_to_end(&mut buffer) .map_err(|e| ZclawError::ToolError(format!("Failed to read file: {}", e)))?; // Return base64 encoded binary content use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; let encoded = BASE64.encode(&buffer); Ok(json!({ "content": encoded, "encoding": "base64", "size": file_size, "path": validated_path.to_string_lossy() })) } _ => { // Text mode (utf-8 or ascii) let mut content = String::with_capacity(file_size as usize); file.read_to_string(&mut content) .map_err(|e| ZclawError::ToolError(format!("Failed to read file: {}", e)))?; Ok(json!({ "content": content, "encoding": encoding, "size": file_size, "path": validated_path.to_string_lossy() })) } } } } impl Default for FileReadTool { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; use crate::tool::builtin::PathValidator; #[tokio::test] async fn test_read_file() { let mut temp_file = NamedTempFile::new().unwrap(); writeln!(temp_file, "Hello, World!").unwrap(); let path = temp_file.path().to_str().unwrap(); let input = json!({ "path": path }); // Configure PathValidator to allow temp directory (use canonicalized path) let temp_dir = std::env::temp_dir().canonicalize().unwrap_or(std::env::temp_dir()); let path_validator = Some(PathValidator::new().with_workspace(temp_dir)); let context = ToolContext { agent_id: zclaw_types::AgentId::new(), working_directory: None, session_id: None, skill_executor: None, path_validator, }; let tool = FileReadTool::new(); let result = tool.execute(input, &context).await.unwrap(); assert!(result["content"].as_str().unwrap().contains("Hello, World!")); assert_eq!(result["encoding"].as_str().unwrap(), "utf-8"); } }