use super::*;
use std::path::PathBuf;
fn ctx_with_allowed(root: PathBuf, allowed: Vec<PathBuf>) -> ToolContext {
ToolContext {
session_id: "test-sandbox".into(),
agent_id: "test-agent".into(),
agent_name: "test-agent".into(),
authority: InputAuthority::Creator,
workspace_root: root,
tool_allowed_paths: allowed,
channel: None,
db: None,
sandbox: ToolSandboxSnapshot::default(),
}
}
fn setup_allowed_dir() -> (tempfile::TempDir, PathBuf, tempfile::TempDir, PathBuf) {
let ws_dir = tempfile::tempdir().unwrap();
let ws_root = std::fs::canonicalize(ws_dir.path()).unwrap();
std::fs::write(ws_root.join("local.txt"), "workspace file").unwrap();
let ext_dir = tempfile::tempdir().unwrap();
let ext_root = std::fs::canonicalize(ext_dir.path()).unwrap();
std::fs::write(ext_root.join("external.txt"), "external file").unwrap();
std::fs::create_dir_all(ext_root.join("sub")).unwrap();
std::fs::write(ext_root.join("sub/nested.txt"), "nested").unwrap();
(ws_dir, ws_root, ext_dir, ext_root)
}
#[tokio::test]
async fn read_file_respects_allowed_paths() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![ext_root.clone()]);
let tool = ReadFileTool;
let result = tool
.execute(
serde_json::json!({"path": ext_root.join("external.txt").to_str().unwrap()}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
assert!(result.unwrap().output.contains("external file"));
}
#[tokio::test]
async fn read_file_blocks_unallowed_absolute() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![]);
let tool = ReadFileTool;
let result = tool
.execute(
serde_json::json!({"path": ext_root.join("external.txt").to_str().unwrap()}),
&ctx,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn write_file_respects_allowed_paths() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![ext_root.clone()]);
let tool = WriteFileTool;
let target = ext_root.join("new_file.txt");
let result = tool
.execute(
serde_json::json!({
"path": target.to_str().unwrap(),
"content": "written via tool"
}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
assert_eq!(
std::fs::read_to_string(&target).unwrap(),
"written via tool"
);
}
#[tokio::test]
async fn write_file_blocks_unallowed_path() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![]); let tool = WriteFileTool;
let result = tool
.execute(
serde_json::json!({
"path": ext_root.join("sneaky.txt").to_str().unwrap(),
"content": "nope"
}),
&ctx,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn list_dir_respects_allowed_paths() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![ext_root.clone()]);
let tool = ListDirectoryTool;
let result = tool
.execute(
serde_json::json!({"path": ext_root.to_str().unwrap()}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
assert!(result.unwrap().output.contains("external.txt"));
}
#[tokio::test]
async fn list_dir_blocks_unallowed_path() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![]); let tool = ListDirectoryTool;
let result = tool
.execute(
serde_json::json!({"path": ext_root.to_str().unwrap()}),
&ctx,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn search_files_respects_allowed_paths() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![ext_root.clone()]);
let tool = SearchFilesTool;
let result = tool
.execute(
serde_json::json!({
"path": ext_root.to_str().unwrap(),
"query": "external"
}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
}
#[tokio::test]
async fn grep_tool_respects_allowed_paths() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![ext_root.clone()]);
let tool = GlobFilesTool;
let result = tool
.execute(
serde_json::json!({
"path": ext_root.to_str().unwrap(),
"pattern": "*.txt"
}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
}
#[tokio::test]
async fn bash_cwd_in_allowed_path_ok() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![ext_root.clone()]);
let tool = BashTool;
let result = tool
.execute(
serde_json::json!({
"command": "ls",
"cwd": ext_root.to_str().unwrap()
}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
assert!(result.unwrap().output.contains("external.txt"));
}
#[tokio::test]
async fn bash_cwd_outside_allowed_blocked() {
let (_ws, ws_root, _ext, ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![]);
let tool = BashTool;
let result = tool
.execute(
serde_json::json!({
"command": "ls",
"cwd": ext_root.to_str().unwrap()
}),
&ctx,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn bash_cwd_workspace_relative_ok() {
let (_ws, ws_root, _ext, _ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![]);
let tool = BashTool;
let result = tool
.execute(
serde_json::json!({
"command": "echo ok",
"cwd": "."
}),
&ctx,
)
.await;
assert!(result.is_ok(), "got: {:?}", result);
assert!(result.unwrap().output.contains("ok"));
}
#[tokio::test]
async fn bash_cwd_traversal_blocked() {
let (_ws, ws_root, _ext, _ext_root) = setup_allowed_dir();
let ctx = ctx_with_allowed(ws_root, vec![]);
let tool = BashTool;
let result = tool
.execute(
serde_json::json!({
"command": "echo pwned",
"cwd": "../../tmp"
}),
&ctx,
)
.await;
assert!(result.is_err());
}