use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use harn_hostlib::tools::permissions;
use harn_hostlib::{
ast::AstCapability, fs::FsCapability, tools::ToolsCapability, BuiltinRegistry,
HostlibCapability, HostlibError,
};
use harn_vm::orchestration::{
pop_execution_policy, push_execution_policy, CapabilityPolicy, RunExecutionRecord,
SandboxProfile,
};
use harn_vm::stdlib::process::set_thread_execution_context;
use harn_vm::VmValue;
use tempfile::TempDir;
fn registry() -> BuiltinRegistry {
permissions::reset();
permissions::enable_for_test();
let mut registry = BuiltinRegistry::new();
ToolsCapability.register_builtins(&mut registry);
FsCapability.register_builtins(&mut registry);
AstCapability.register_builtins(&mut registry);
registry
}
struct PolicyGuard;
impl PolicyGuard {
fn worktree(roots: &[&Path]) -> Self {
push_execution_policy(CapabilityPolicy {
sandbox_profile: SandboxProfile::Worktree,
workspace_roots: roots
.iter()
.map(|root| root.to_string_lossy().into_owned())
.collect(),
..CapabilityPolicy::default()
});
PolicyGuard
}
}
impl Drop for PolicyGuard {
fn drop(&mut self) {
pop_execution_policy();
set_thread_execution_context(None);
}
}
struct ExecutionRootGuard;
impl ExecutionRootGuard {
fn push(root: &Path) -> Self {
let root = path_string(root);
set_thread_execution_context(Some(RunExecutionRecord {
cwd: Some(root.clone()),
project_root: Some(root.clone()),
source_dir: Some(root),
env: BTreeMap::new(),
adapter: None,
repo_path: None,
worktree_path: None,
branch: None,
base_ref: None,
cleanup: None,
}));
ExecutionRootGuard
}
}
impl Drop for ExecutionRootGuard {
fn drop(&mut self) {
set_thread_execution_context(None);
}
}
fn dict_arg(entries: &[(&str, VmValue)]) -> Vec<VmValue> {
let mut map: harn_vm::value::DictMap = Default::default();
for (k, v) in entries {
map.insert(harn_vm::value::intern_key(k), v.clone());
}
vec![VmValue::dict(map)]
}
fn vm_string(s: &str) -> VmValue {
VmValue::String(arcstr::ArcStr::from(s))
}
fn path_string(p: &Path) -> String {
p.to_string_lossy().into_owned()
}
fn call(
reg: &BuiltinRegistry,
name: &str,
entries: &[(&str, VmValue)],
) -> Result<VmValue, HostlibError> {
let entry = reg
.find(name)
.unwrap_or_else(|| panic!("{name} registered"));
(entry.handler)(&dict_arg(entries))
}
fn assert_rejected(result: Result<VmValue, HostlibError>, expect_builtin: &str) {
match result {
Err(HostlibError::SandboxViolation {
builtin, message, ..
}) => {
assert_eq!(builtin, expect_builtin, "violation names the builtin");
assert!(
message.contains("outside workspace_roots"),
"message describes the scope rejection: {message}"
);
}
other => panic!("expected SandboxViolation from {expect_builtin}, got {other:?}"),
}
}
#[test]
fn read_write_delete_list_respect_workspace_roots() {
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let in_file = root.path().join("in.txt");
fs::write(&in_file, "hello").unwrap();
let out_file = outside.path().join("out.txt");
fs::write(&out_file, "secret").unwrap();
let reg = registry();
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_tools_read_file",
&[("path", vm_string(&path_string(&in_file)))],
)
.expect("in-root read succeeds");
assert_rejected(
call(
®,
"hostlib_tools_read_file",
&[("path", vm_string(&path_string(&out_file)))],
),
"hostlib_tools_read_file",
);
call(
®,
"hostlib_tools_list_directory",
&[("path", vm_string(&path_string(root.path())))],
)
.expect("in-root list succeeds");
assert_rejected(
call(
®,
"hostlib_tools_list_directory",
&[("path", vm_string(&path_string(outside.path())))],
),
"hostlib_tools_list_directory",
);
let new_in = root.path().join("created.txt");
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&path_string(&new_in))),
("content", vm_string("x")),
],
)
.expect("in-root write succeeds");
assert!(new_in.exists());
let new_out = outside.path().join("created.txt");
assert_rejected(
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&path_string(&new_out))),
("content", vm_string("x")),
],
),
"hostlib_tools_write_file",
);
assert!(!new_out.exists(), "rejected write must not touch disk");
assert_rejected(
call(
®,
"hostlib_tools_delete_file",
&[("path", vm_string(&path_string(&out_file)))],
),
"hostlib_tools_delete_file",
);
assert!(out_file.exists(), "rejected delete must not touch disk");
call(
®,
"hostlib_tools_delete_file",
&[("path", vm_string(&path_string(&in_file)))],
)
.expect("in-root delete succeeds");
assert!(!in_file.exists());
}
#[test]
fn search_and_outline_respect_workspace_roots() {
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
fs::write(root.path().join("a.rs"), "fn alpha() {}\n").unwrap();
fs::write(outside.path().join("b.rs"), "fn beta() {}\n").unwrap();
let reg = registry();
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_tools_search",
&[
("pattern", vm_string("fn ")),
("path", vm_string(&path_string(root.path()))),
],
)
.expect("in-root search succeeds");
assert_rejected(
call(
®,
"hostlib_tools_search",
&[
("pattern", vm_string("fn ")),
("path", vm_string(&path_string(outside.path()))),
],
),
"hostlib_tools_search",
);
call(
®,
"hostlib_tools_get_file_outline",
&[("path", vm_string(&path_string(&root.path().join("a.rs"))))],
)
.expect("in-root outline succeeds");
assert_rejected(
call(
®,
"hostlib_tools_get_file_outline",
&[(
"path",
vm_string(&path_string(&outside.path().join("b.rs"))),
)],
),
"hostlib_tools_get_file_outline",
);
}
#[test]
fn safe_text_patch_and_read_text_respect_workspace_roots() {
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let in_file = root.path().join("p.txt");
fs::write(&in_file, "v1\n").unwrap();
let out_file = outside.path().join("p.txt");
fs::write(&out_file, "v1\n").unwrap();
let reg = registry();
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_fs_read_text",
&[("path", vm_string(&path_string(&in_file)))],
)
.expect("in-root read_text succeeds");
assert_rejected(
call(
®,
"hostlib_fs_read_text",
&[("path", vm_string(&path_string(&out_file)))],
),
"hostlib_fs_read_text",
);
call(
®,
"hostlib_fs_safe_text_patch",
&[
("path", vm_string(&path_string(&in_file))),
("content", vm_string("v2\n")),
],
)
.expect("in-root patch succeeds");
assert_eq!(fs::read_to_string(&in_file).unwrap(), "v2\n");
assert_rejected(
call(
®,
"hostlib_fs_safe_text_patch",
&[
("path", vm_string(&path_string(&out_file))),
("content", vm_string("v2\n")),
],
),
"hostlib_fs_safe_text_patch",
);
assert_eq!(
fs::read_to_string(&out_file).unwrap(),
"v1\n",
"rejected patch must not touch disk"
);
}
#[test]
fn ast_edits_respect_workspace_roots() {
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let src = "fn alpha() { return 1 }\n";
let in_file = root.path().join("edit.rs");
fs::write(&in_file, src).unwrap();
let out_file = outside.path().join("edit.rs");
fs::write(&out_file, src).unwrap();
let reg = registry();
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_ast_apply_node",
&[
("path", vm_string(&path_string(&in_file))),
("query", vm_string("(integer_literal) @target")),
("replacement", vm_string("2")),
],
)
.expect("in-root apply_node succeeds");
assert!(fs::read_to_string(&in_file).unwrap().contains("return 2"));
assert_rejected(
call(
®,
"hostlib_ast_apply_node",
&[
("path", vm_string(&path_string(&out_file))),
("query", vm_string("(integer_literal) @target")),
("replacement", vm_string("2")),
],
),
"hostlib_ast_apply_node",
);
assert_eq!(
fs::read_to_string(&out_file).unwrap(),
src,
"rejected AST edit must not touch disk"
);
assert_rejected(
call(
®,
"hostlib_ast_insert_at_anchor",
&[
("path", vm_string(&path_string(&out_file))),
("query", vm_string("(function_item) @anchor")),
("content", vm_string("// added\n")),
("position", vm_string("before")),
],
),
"hostlib_ast_insert_at_anchor",
);
}
#[test]
fn staged_commit_enforces_scope_against_target_path() {
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let session = "fs-path-scope-commit";
let in_target = root.path().join("staged_in.txt");
let out_target = outside.path().join("staged_out.txt");
let reg = registry();
call(
®,
"hostlib_fs_set_mode",
&[
("session_id", vm_string(session)),
("mode", vm_string("staged")),
("root", vm_string(&path_string(root.path()))),
],
)
.expect("set staged mode");
call(
®,
"hostlib_tools_write_file",
&[
("session_id", vm_string(session)),
("path", vm_string(&path_string(&in_target))),
("content", vm_string("in\n")),
],
)
.expect("stage in-root write");
call(
®,
"hostlib_tools_write_file",
&[
("session_id", vm_string(session)),
("path", vm_string(&path_string(&out_target))),
("content", vm_string("out\n")),
],
)
.expect("stage out-of-root write");
let result = {
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_fs_commit_staged",
&[("session_id", vm_string(session))],
)
.expect("commit returns a result envelope")
};
assert!(in_target.exists(), "in-root target flushed to disk");
assert!(!out_target.exists(), "out-of-root target must not flush");
let committed = match dict_field(&result, "committed_paths") {
VmValue::List(items) => items.clone(),
other => panic!("committed_paths is a list, got {other:?}"),
};
assert_eq!(committed.len(), 1, "exactly one path committed");
let failed = match dict_field(&result, "failed_paths_with_reasons") {
VmValue::List(items) => items.clone(),
other => panic!("failed_paths_with_reasons is a list, got {other:?}"),
};
assert_eq!(failed.len(), 1, "the out-of-root target is reported failed");
let reason = dict_field(&failed[0], "reason");
match reason {
VmValue::String(s) => assert!(
s.contains("outside workspace_roots"),
"failure reason is the scope rejection: {s}"
),
other => panic!("reason is a string, got {other:?}"),
}
call(
®,
"hostlib_fs_discard_staged",
&[("session_id", vm_string(session))],
)
.ok();
}
#[test]
fn relative_paths_are_resolved_before_the_scope_check() {
let root = std::env::current_dir().unwrap();
let reg = registry();
let _guard = PolicyGuard::worktree(&[root.as_path()]);
call(
®,
"hostlib_tools_read_file",
&[("path", vm_string("src/../Cargo.toml"))],
)
.expect("relative in-root read succeeds after normalization");
assert_rejected(
call(
®,
"hostlib_tools_read_file",
&[("path", vm_string("../harn-vm/Cargo.toml"))],
),
"hostlib_tools_read_file",
);
}
#[test]
fn write_surfaces_resolve_relative_noncanonical_paths_against_execution_root() {
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("src")).unwrap();
let unique = format!("hostlib-scope-{}", uuid::Uuid::now_v7());
let write_rel = format!("src/../generated/{unique}-tools.txt");
let patch_rel = format!("src/../generated/{unique}-patch.txt");
let escaped_rel = format!("../{unique}-escape.txt");
let process_cwd_shadow = std::env::current_dir()
.unwrap()
.join("generated")
.join(format!("{unique}-tools.txt"));
assert!(
!process_cwd_shadow.exists(),
"test fixture should start without the process-cwd shadow path"
);
let escaped_target = root
.path()
.parent()
.unwrap()
.join(format!("{unique}-escape.txt"));
assert!(
!escaped_target.exists(),
"test fixture should start without the sibling escape path"
);
let reg = registry();
let _execution = ExecutionRootGuard::push(root.path());
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&write_rel)),
("content", vm_string("tool write\n")),
],
)
.expect("relative non-canonical tool write succeeds in execution root");
assert_eq!(
fs::read_to_string(root.path().join(format!("generated/{unique}-tools.txt"))).unwrap(),
"tool write\n"
);
assert!(
!process_cwd_shadow.exists(),
"relative hostlib write must not land under the process cwd"
);
call(
®,
"hostlib_fs_safe_text_patch",
&[
("path", vm_string(&patch_rel)),
("content", vm_string("safe patch\n")),
],
)
.expect("relative non-canonical safe_text_patch succeeds in execution root");
assert_eq!(
fs::read_to_string(root.path().join(format!("generated/{unique}-patch.txt"))).unwrap(),
"safe patch\n"
);
assert_rejected(
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&escaped_rel)),
("content", vm_string("escape\n")),
],
),
"hostlib_tools_write_file",
);
assert_rejected(
call(
®,
"hostlib_fs_safe_text_patch",
&[
("path", vm_string(&escaped_rel)),
("content", vm_string("escape\n")),
],
),
"hostlib_fs_safe_text_patch",
);
assert!(
!escaped_target.exists(),
"out-of-root relative writes must not touch disk"
);
}
fn dict_field<'a>(value: &'a VmValue, key: &str) -> &'a VmValue {
match value {
VmValue::Dict(d) => d.get(key).unwrap_or_else(|| panic!("key {key} present")),
other => panic!("not a dict: {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn write_delete_reject_symlink_swap_escape() {
use std::os::unix::fs::symlink;
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let secret = outside.path().join("secret.txt");
fs::write(&secret, "original-secret\n").unwrap();
let evil_link = root.path().join("evil_link.txt");
symlink(&secret, &evil_link).unwrap();
let reg = registry();
let _guard = PolicyGuard::worktree(&[root.path()]);
let write_res = call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&path_string(&evil_link))),
("content", vm_string("attacker-payload\n")),
],
);
assert!(
write_res.is_err(),
"write through an escaping symlink must be rejected, got {write_res:?}"
);
assert_eq!(
fs::read_to_string(&secret).unwrap(),
"original-secret\n",
"the outside target must not be modified through the symlink"
);
let delete_res = call(
®,
"hostlib_tools_delete_file",
&[("path", vm_string(&path_string(&evil_link)))],
);
assert!(
delete_res.is_err(),
"delete through an escaping symlink must be rejected, got {delete_res:?}"
);
assert!(
secret.exists(),
"the outside target must not be deleted through the symlink"
);
let in_root_target = root.path().join("victim.txt");
fs::write(&in_root_target, "victim-original\n").unwrap();
let benign_link = root.path().join("benign_link.txt");
symlink(&in_root_target, &benign_link).unwrap();
let write_through_in_root = call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&path_string(&benign_link))),
("content", vm_string("payload-via-symlink\n")),
],
);
assert!(
write_through_in_root.is_err(),
"the no-follow guard must refuse to write through a symlink-final path, got \
{write_through_in_root:?}"
);
assert_eq!(
fs::read_to_string(&in_root_target).unwrap(),
"victim-original\n",
"the symlink's target must not be modified by writing through the link"
);
let real = root.path().join("real.txt");
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&path_string(&real))),
("content", vm_string("v1\n")),
],
)
.expect("in-root real-file create succeeds");
assert_eq!(fs::read_to_string(&real).unwrap(), "v1\n");
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string(&path_string(&real))),
("content", vm_string("v2\n")),
],
)
.expect("in-root real-file overwrite succeeds");
assert_eq!(fs::read_to_string(&real).unwrap(), "v2\n");
call(
®,
"hostlib_tools_delete_file",
&[("path", vm_string(&path_string(&real)))],
)
.expect("in-root real-file delete succeeds");
assert!(!real.exists());
}
#[cfg(unix)]
#[test]
fn write_surfaces_respect_symlinked_intermediate_directories() {
use std::os::unix::fs::symlink;
let root = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let real_dir = root.path().join("real");
fs::create_dir(&real_dir).unwrap();
symlink(&real_dir, root.path().join("inside_link")).unwrap();
symlink(outside.path(), root.path().join("outside_link")).unwrap();
let reg = registry();
let _execution = ExecutionRootGuard::push(root.path());
let _guard = PolicyGuard::worktree(&[root.path()]);
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string("inside_link/tool.txt")),
("content", vm_string("through in-root symlink\n")),
],
)
.expect("in-root symlinked intermediate tool write succeeds");
assert_eq!(
fs::read_to_string(real_dir.join("tool.txt")).unwrap(),
"through in-root symlink\n"
);
call(
®,
"hostlib_fs_safe_text_patch",
&[
("path", vm_string("inside_link/patch.txt")),
("content", vm_string("through in-root symlink patch\n")),
],
)
.expect("in-root symlinked intermediate safe_text_patch succeeds");
assert_eq!(
fs::read_to_string(real_dir.join("patch.txt")).unwrap(),
"through in-root symlink patch\n"
);
assert_rejected(
call(
®,
"hostlib_tools_write_file",
&[
("path", vm_string("outside_link/tool.txt")),
("content", vm_string("escape\n")),
],
),
"hostlib_tools_write_file",
);
assert!(
!outside.path().join("tool.txt").exists(),
"tool write must not follow an escaping intermediate symlink"
);
assert_rejected(
call(
®,
"hostlib_fs_safe_text_patch",
&[
("path", vm_string("outside_link/patch.txt")),
("content", vm_string("escape\n")),
],
),
"hostlib_fs_safe_text_patch",
);
assert!(
!outside.path().join("patch.txt").exists(),
"safe_text_patch must not follow an escaping intermediate symlink"
);
}