#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use std::sync::atomic::{AtomicU64, Ordering};
use polyc_agent::ToolExecutor;
use polyc_agent::delegate::WorkerScope;
use polyc_tools::ToolRegistry;
fn tmp_root(prefix: &str) -> std::path::PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
let n = SEQ.fetch_add(1, Ordering::Relaxed);
p.push(format!("pc-{prefix}-{}-{n}", std::process::id()));
std::fs::create_dir_all(&p).expect("temp root is creatable");
p
}
fn registry_at(root: &std::path::Path) -> ToolRegistry {
ToolRegistry::rooted_at(root.to_path_buf(), None)
}
async fn write_file(tools: &dyn ToolExecutor, path: &str, content: &str) -> String {
let args = serde_json::json!({ "path": path, "content": content }).to_string();
tools.execute("file_write", &args).await
}
async fn read_file(tools: &dyn ToolExecutor, path: &str) -> String {
let args = serde_json::json!({ "path": path }).to_string();
tools.execute("file_read", &args).await
}
fn files_under(root: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if let Ok(rel) = path.strip_prefix(root) {
out.push(rel.to_path_buf());
}
}
}
out.sort();
out
}
#[tokio::test]
async fn concurrent_workers_do_not_clobber_the_same_relative_path() {
let root = tmp_root("fence-clobber");
let parent = registry_at(&root);
let worker_a = parent
.for_worker(&WorkerScope::bare("call-a"))
.expect("a registry owns a workspace, so it must re-root")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
let worker_b = parent
.for_worker(&WorkerScope::bare("call-b"))
.expect("a registry owns a workspace, so it must re-root")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
let (ra, rb) = tokio::join!(
write_file(worker_a.as_ref(), "out.txt", "written-by-a"),
write_file(worker_b.as_ref(), "out.txt", "written-by-b"),
);
for (who, result) in [("a", &ra), ("b", &rb)] {
let v: serde_json::Value = serde_json::from_str(result).expect("JSON result");
assert!(
v.get("error").is_none(),
"worker {who}: write should succeed, got {result}"
);
}
let files = files_under(&root);
assert_eq!(
files.len(),
2,
"each worker must get its own out.txt; found {files:?}"
);
for rel in &files {
assert_eq!(
rel.file_name().and_then(std::ffi::OsStr::to_str),
Some("out.txt")
);
assert!(
rel.parent().is_some_and(|p| p != std::path::Path::new("")),
"each write must land in a per-worker subdirectory, got {rel:?}"
);
}
assert_ne!(files[0], files[1], "the two workers shared a path");
let back_a = read_file(worker_a.as_ref(), "out.txt").await;
let back_b = read_file(worker_b.as_ref(), "out.txt").await;
assert!(
back_a.contains("written-by-a") && !back_a.contains("written-by-b"),
"worker a read the wrong bytes: {back_a}"
);
assert!(
back_b.contains("written-by-b") && !back_b.contains("written-by-a"),
"worker b read the wrong bytes: {back_b}"
);
std::fs::remove_dir_all(&root).ok();
}
#[tokio::test]
async fn worker_cannot_read_a_file_the_parent_wrote() {
let root = tmp_root("fence-reads");
let parent = registry_at(&root);
let written = write_file(&parent, "parent.txt", "parent-bytes").await;
let v: serde_json::Value = serde_json::from_str(&written).expect("JSON result");
assert!(v.get("error").is_none(), "parent write failed: {written}");
assert!(root.join("parent.txt").exists());
let worker = parent
.for_worker(&WorkerScope::bare("call-a"))
.expect("must re-root")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
let out = read_file(worker.as_ref(), "parent.txt").await;
assert!(
!out.contains("parent-bytes"),
"a fenced worker must not see the parent's file: {out}"
);
let parent_back = read_file(&parent, "parent.txt").await;
assert!(
parent_back.contains("parent-bytes"),
"the parent lost its own file: {parent_back}"
);
std::fs::remove_dir_all(&root).ok();
}
#[tokio::test]
async fn traversal_is_still_refused_from_inside_a_worker_root() {
let root = tmp_root("fence-traversal");
let parent = registry_at(&root);
let worker = parent
.for_worker(&WorkerScope::bare("call-a"))
.expect("must re-root")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
for path in ["../escape.txt", "../../etc/passwd", "/etc/passwd", ".."] {
let out = write_file(worker.as_ref(), path, "x").await;
let v: serde_json::Value = serde_json::from_str(&out).expect("JSON result");
assert!(
v.get("error").is_some(),
"worker write to {path} must be refused, got {out}"
);
}
assert!(!root.join("escape.txt").exists());
std::fs::remove_dir_all(&root).ok();
}
#[tokio::test]
async fn hostile_worker_ids_stay_one_level_under_the_conversation_root() {
let root = tmp_root("fence-ids");
let parent = registry_at(&root);
let hostile = ["../../etc", "a/b", "a_b", "", "..", "."];
let mut dirs = Vec::new();
for id in hostile {
let worker = parent
.for_worker(&WorkerScope::bare(id))
.unwrap_or_else(|| panic!("re-root failed for {id:?}"))
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
let out = write_file(worker.as_ref(), "probe.txt", id).await;
let v: serde_json::Value = serde_json::from_str(&out).expect("JSON result");
assert!(v.get("error").is_none(), "{id:?}: write failed: {out}");
let files = files_under(&root);
let found = files
.iter()
.find(|f| std::fs::read_to_string(root.join(f)).is_ok_and(|c| c == id))
.unwrap_or_else(|| panic!("{id:?}: no file carried its bytes"));
assert_eq!(
found.components().count(),
2,
"{id:?} must land exactly one level deep, got {found:?}"
);
dirs.push(
found
.parent()
.expect("a per-worker file always has a parent dir")
.to_path_buf(),
);
}
let mut unique = dirs.clone();
unique.sort();
unique.dedup();
assert_eq!(
unique.len(),
dirs.len(),
"distinct worker ids collapsed into one directory: {dirs:?}"
);
std::fs::remove_dir_all(&root).ok();
}
#[tokio::test]
async fn a_failed_subtree_creation_still_re_roots_away_from_the_shared_root() {
let root = tmp_root("fence-faildir");
let parent = registry_at(&root);
let subdir = polyc_tools::coding::workspace::worker_root(&root, "call-a");
std::fs::write(&subdir, b"in the way").expect("the blocking file is writable");
assert!(
subdir.is_file(),
"the worker path must be a file, not a dir"
);
let worker = parent
.for_worker(&WorkerScope::bare("call-a"))
.expect("a failed mkdir must not report `no workspace to re-root`")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
let result = write_file(&*worker, "out.txt", "worker bytes").await;
assert!(
!root.join("out.txt").exists(),
"the worker escaped to the SHARED conversation root: {result}"
);
std::fs::remove_dir_all(&root).ok();
}
#[tokio::test]
async fn a_parent_search_does_not_descend_into_worker_subtrees() {
let root = tmp_root("fence-search");
let parent = registry_at(&root);
write_file(&parent, "project.txt", "needle in the project").await;
let worker = parent
.for_worker(&WorkerScope::bare("call-a"))
.expect("a registry owns a workspace, so it must re-root")
.expect("a bare scope requests no seeding, so nothing can be refused")
.tools;
write_file(&*worker, "scratch.txt", "needle in the worker scratch").await;
let hits = parent
.execute(
"glob",
&serde_json::json!({ "pattern": "**/*.txt" }).to_string(),
)
.await;
assert!(
hits.contains("project.txt"),
"the parent must still find its own files: {hits}"
);
assert!(
!hits.contains("scratch.txt"),
"the parent walked into a worker's subtree: {hits}"
);
let own = worker
.execute(
"glob",
&serde_json::json!({ "pattern": "**/*.txt" }).to_string(),
)
.await;
assert!(
own.contains("scratch.txt"),
"a worker must still see its own scratch files: {own}"
);
std::fs::remove_dir_all(&root).ok();
}