Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
style: 统一代码格式和注释风格 docs: 更新多个功能文档的完整度和状态 feat(runtime): 添加路径验证工具支持 fix(pipeline): 改进条件判断和变量解析逻辑 test(types): 为ID类型添加全面测试用例 chore: 更新依赖项和Cargo.lock文件 perf(mcp): 优化MCP协议传输和错误处理
152 lines
4.8 KiB
Rust
152 lines
4.8 KiB
Rust
//! 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<Value> {
|
|
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");
|
|
}
|
|
}
|