use crate::system::fs::FileSystem;
use crate::tools::{Tool, ToolError, ToolResult};
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::Value;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Deserialize)]
struct ListArgs {
path: String,
}
pub struct ListTool {
fs: Arc<dyn FileSystem>,
workspace_root: PathBuf,
}
impl ListTool {
pub fn new(fs: Arc<dyn FileSystem>, workspace_root: PathBuf) -> anyhow::Result<Self> {
let root = workspace_root
.canonicalize()
.map_err(|e| anyhow::anyhow!("Invalid workspace root: {}", e))?;
Ok(Self {
fs,
workspace_root: root,
})
}
}
#[async_trait]
impl Tool for ListTool {
fn name(&self) -> &str {
"ls"
}
fn description(&self) -> &str {
"List files and directories in a given path. Sandboxed to workspace."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Relative path to list."
}
},
"required": ["path"]
})
}
async fn execute(&self, args: Value) -> ToolResult<Value> {
let args: ListArgs =
serde_json::from_value(args).map_err(|e| ToolError::InvalidArguments(e.to_string()))?;
let target_path = self.workspace_root.join(args.path);
let canonical_target = target_path
.canonicalize()
.map_err(|e| ToolError::ExecutionError(format!("Invalid path: {}", e)))?;
if !canonical_target.starts_with(&self.workspace_root) {
return Err(ToolError::ExecutionError(
"Security Violation: Path traversal attempted".to_string(),
));
}
let entries = self
.fs
.list_directory(&canonical_target)
.await
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
Ok(serde_json::json!({ "entries": entries }))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::fs::MockFileSystem;
#[tokio::test]
async fn test_ls_tool_hardened() {
let mut mock_fs = MockFileSystem::new();
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
mock_fs
.expect_list_directory()
.times(1)
.returning(|_| Box::pin(async move { Ok(vec!["file1.txt".to_string()]) }));
let tool = ListTool::new(Arc::new(mock_fs), root.clone()).unwrap();
let args = serde_json::json!({"path": "."});
let result = tool.execute(args).await;
assert!(result.is_ok());
}
}