use crate::tools::registry::Tool;
use serde_json::Value;
use std::path::{Path, PathBuf};
const MAX_FILE_SIZE: u64 = 1024 * 1024;
pub struct ReadFileTool {
pub allowed_dirs: Vec<PathBuf>,
}
impl Tool for ReadFileTool {
fn name(&self) -> &str {
"read_file"
}
fn description(&self) -> &str {
"Reads the contents of a local file and returns it as a string."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The absolute path to the file to read."
}
},
"required": ["path"]
})
}
fn execute<'a>(
&'a self,
input: Value,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value, String>> + Send + 'a>>
{
let allowed_dirs = self.allowed_dirs.clone();
Box::pin(async move {
let raw_path = input
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing 'path' argument".to_string())?;
let path = Path::new(raw_path);
if !path.is_absolute() {
return Err("Only absolute paths are allowed".to_string());
}
let canonical = path
.canonicalize()
.map_err(|e| format!("Cannot resolve path '{}': {}", raw_path, e))?;
if !allowed_dirs.is_empty() {
let permitted = allowed_dirs.iter().any(|base| canonical.starts_with(base));
if !permitted {
return Err(format!(
"Path '{}' is outside the configured allowed directories",
raw_path
));
}
} else {
return Err(
"read_file requires allowed_dirs to be configured in builtin_tools config. \
Set allowed_dirs: [\"/\"] to explicitly allow unrestricted access."
.to_string(),
);
}
let metadata = std::fs::metadata(&canonical)
.map_err(|e| format!("Cannot stat '{}': {}", raw_path, e))?;
if metadata.len() > MAX_FILE_SIZE {
return Err(format!(
"File is {} bytes, exceeds {} byte limit",
metadata.len(),
MAX_FILE_SIZE
));
}
match std::fs::read_to_string(&canonical) {
Ok(content) => Ok(serde_json::json!({ "content": content })),
Err(e) => Err(format!("Failed to read file '{}': {}", raw_path, e)),
}
})
}
}