#![allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU32, Ordering};
const BINARY: &str = env!("CARGO_BIN_EXE_ralon");
const POLICY: &str = "version: 1\nprotect:\n - .env\n - src/index.tsx\n - config/**\n";
struct Machine {
home: PathBuf,
code: PathBuf,
projects: std::cell::RefCell<Vec<PathBuf>>,
}
impl Machine {
fn new() -> Machine {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let root = std::env::temp_dir().join(format!(
"ralon-sup-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
let home = root.join("state");
let code = root.join("code");
fs::create_dir_all(&home).unwrap();
fs::create_dir_all(&code).unwrap();
let machine = Machine {
home,
code,
projects: std::cell::RefCell::new(Vec::new()),
};
machine.configure(std::slice::from_ref(&machine.code));
machine
}
fn configure(&self, roots: &[PathBuf]) {
self.write_config(roots, true);
}
fn set_hooks(&self, hooks: bool) {
self.write_config(std::slice::from_ref(&self.code), hooks);
}
fn write_config(&self, roots: &[PathBuf], hooks: bool) {
let mut text = String::from("roots:\n");
for root in roots {
let canonical = fs::canonicalize(root).unwrap_or_else(|_| root.clone());
text.push_str(&format!("- {}\n", yaml(&canonical)));
}
text.push_str("max_depth: 8\n");
text.push_str(&format!("hooks: {hooks}\n"));
fs::write(self.home.join("config.yaml"), text).unwrap();
}
fn elsewhere(&self, name: &str) -> PathBuf {
let root = self
.home
.parent()
.expect("the state directory has a parent");
let path = root.join(name);
fs::create_dir_all(&path).unwrap();
plain(&fs::canonicalize(&path).unwrap())
}
fn repository(&self, name: &str, policy: Option<&str>) -> Repository {
self.repository_in(&self.code.clone(), name, policy)
}
fn repository_in(&self, parent: &Path, name: &str, policy: Option<&str>) -> Repository {
let root = parent.join(name);
fs::create_dir_all(root.join("src")).unwrap();
fs::create_dir_all(root.join("config")).unwrap();
fs::write(root.join(".env"), "SECRET=original").unwrap();
fs::write(root.join("src/index.tsx"), "original").unwrap();
fs::write(root.join("src/App.tsx"), "original").unwrap();
fs::write(root.join("config/db.yaml"), "original").unwrap();
let repository = Repository {
root: plain(&fs::canonicalize(&root).unwrap()),
home: self.home.clone(),
};
self.projects.borrow_mut().push(repository.root.clone());
if let Some(policy) = policy {
repository.declare(policy);
}
repository
}
fn ralon(&self, arguments: &[&str]) -> Output {
Command::new(BINARY)
.args(arguments)
.env("RALON_HOME", &self.home)
.output()
.expect("failed to run ralon")
}
fn tick(&self) -> Output {
self.ralon(&["daemon", "--once"])
}
fn recorded(&self) -> String {
fs::read_to_string(self.home.join("workspaces.json")).unwrap_or_default()
}
}
impl Drop for Machine {
fn drop(&mut self) {
for root in self.projects.borrow().iter() {
let _ = Command::new(BINARY)
.arg("--dir")
.arg(root)
.args(["guard", "--stop"])
.env("RALON_HOME", &self.home)
.output();
}
if let Some(parent) = self.home.parent() {
let _ = fs::remove_dir_all(parent);
}
}
}
struct Repository {
root: PathBuf,
home: PathBuf,
}
impl Repository {
fn declare(&self, policy: &str) {
fs::write(self.root.join("agent.lock"), policy).unwrap();
}
fn undeclare(&self) {
fs::remove_file(self.root.join("agent.lock")).unwrap();
}
fn path(&self, relative: &str) -> PathBuf {
self.root.join(relative)
}
fn contents(&self, relative: &str) -> String {
fs::read_to_string(self.path(relative)).unwrap_or_default()
}
fn ralon(&self, arguments: &[&str]) -> Output {
Command::new(BINARY)
.arg("--dir")
.arg(&self.root)
.args(arguments)
.env("RALON_HOME", &self.home)
.current_dir(&self.root)
.output()
.expect("failed to run ralon")
}
fn hook(&self, request: &str) -> Output {
use std::io::Write;
let mut child = Command::new(BINARY)
.arg("--dir")
.arg(&self.root)
.args(["hook", "check"])
.env("RALON_HOME", &self.home)
.current_dir(&self.root)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to run ralon");
child
.stdin
.take()
.expect("stdin was piped")
.write_all(request.as_bytes())
.expect("failed to send the request");
child
.wait_with_output()
.expect("failed to read the decision")
}
fn writable(&self, relative: &str) -> bool {
let before = self.contents(relative);
let marker = "OVERWRITTEN-BY-AN-AGENT";
shell(&self.root, &redirect(marker, &self.path(relative)));
let after = self.contents(relative);
if after.trim() == marker {
let _ = fs::write(self.path(relative), before);
return true;
}
false
}
fn creatable(&self, relative: &str) -> bool {
let path = self.path(relative);
let _ = fs::remove_file(&path);
shell(&self.root, &redirect("NEW", &path));
let created = path.is_file();
let _ = fs::remove_file(&path);
created
}
}
fn shell(directory: &Path, command: &str) {
#[cfg(windows)]
let mut process = {
use std::os::windows::process::CommandExt;
let mut process = Command::new("cmd");
process.raw_arg("/c").raw_arg(command);
process
};
#[cfg(not(windows))]
let mut process = {
let mut process = Command::new("sh");
process.args(["-c", command]);
process
};
let _ = process.current_dir(directory).output();
}
fn redirect(text: &str, path: &Path) -> String {
format!("echo {text}> \"{}\"", path.display())
}
fn yaml(path: &Path) -> String {
format!("'{}'", path.display().to_string().replace('\'', "''"))
}
fn plain(path: &Path) -> PathBuf {
let text = path.display().to_string();
match text.strip_prefix(r"\\?\") {
Some(rest) if !rest.starts_with("UNC\\") => PathBuf::from(rest),
_ => path.to_path_buf(),
}
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn code(output: &Output) -> i32 {
output.status.code().expect("process was killed")
}
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn a_policy_outside_every_declared_scope_is_never_enforced() {
let machine = Machine::new();
let empty = machine.code.join("in-scope");
fs::create_dir_all(&empty).unwrap();
machine.configure(&[empty]);
let elsewhere = machine.repository("out-of-scope", Some(POLICY));
machine.tick();
assert!(
!machine.recorded().contains("out-of-scope"),
"a policy outside every scan root was picked up: {}",
machine.recorded()
);
let status = elsewhere.ralon(&["status"]);
assert!(
!stdout(&status).contains("enforced by the supervisor"),
"{}",
stdout(&status)
);
}
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn a_malformed_policy_is_reported_and_never_looks_enforced() {
let machine = Machine::new();
let repository = machine.repository("broken", Some("version: 1\nprotect: [oh no: ["));
let tick = machine.tick();
assert_eq!(code(&tick), 0, "{}", stderr(&tick));
let reported = format!("{}{}", stdout(&tick), stderr(&tick));
assert!(
reported.contains("cannot enforce"),
"a policy that does not parse was not reported: {reported}"
);
assert!(
machine.recorded().contains("failed"),
"{}",
machine.recorded()
);
let status = repository.ralon(&["status"]);
assert_ne!(
code(&status),
0,
"status was happy about a policy it cannot read: {}",
stdout(&status)
);
assert!(
stderr(&status).contains("failed to parse"),
"{}",
stderr(&status)
);
assert!(!stdout(&status).contains("enforced"), "{}", stdout(&status));
}
#[test]
#[cfg(any(windows, target_os = "macos"))]
fn a_malformed_policy_locks_nothing_rather_than_locking_everything() {
let machine = Machine::new();
let repository = machine.repository("broken", Some("version: 1\nprotect: [oh no: ["));
machine.tick();
assert!(
repository.writable(".env"),
"an unreadable policy left files locked, with no way to find out why"
);
assert!(
machine.recorded().contains("failed"),
"{}",
machine.recorded()
);
}
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn a_broken_policy_that_gets_fixed_is_picked_up() {
let machine = Machine::new();
let repository = machine.repository("fixed", Some("version: 9\nprotect: []"));
machine.tick();
assert!(
machine.recorded().contains("failed"),
"{}",
machine.recorded()
);
repository.declare(POLICY);
machine.tick();
assert!(
machine.recorded().contains("enforced"),
"a corrected policy was not picked up: {}",
machine.recorded()
);
}
#[cfg(any(windows, target_os = "macos"))]
#[test]
fn a_policy_file_carries_no_machine_state() {
let machine = Machine::new();
let repository = machine.repository("portable", Some(POLICY));
machine.tick();
assert_eq!(
repository.contents("agent.lock"),
POLICY,
"the supervisor modified agent.lock"
);
let recorded = machine.recorded();
assert!(
recorded.contains("portable"),
"state should live in the state directory: {recorded}"
);
}
#[cfg(not(any(windows, target_os = "macos")))]
mod without_a_supervisor {
use super::*;
#[test]
fn install_refuses_and_explains_why() {
let machine = Machine::new();
let attempt = machine.ralon(&["install", "--scope", machine.code.to_str().unwrap()]);
assert_ne!(
code(&attempt),
0,
"install claimed to work: {}",
stdout(&attempt)
);
let said = stderr(&attempt);
assert!(said.contains("inherited"), "{said}");
assert!(said.contains("ralon run"), "{said}");
assert!(
!said.contains("systemd"),
"a systemd unit would start and enforce nothing: {said}"
);
}
#[test]
fn the_daemon_refuses_to_run_rather_than_tracking_files_it_cannot_protect() {
let machine = Machine::new();
let _repository = machine.repository("app", Some(POLICY));
let attempt = machine.tick();
assert_ne!(
code(&attempt),
0,
"the daemon started on a platform where it can enforce nothing: {}",
stdout(&attempt)
);
assert!(
!machine.home.join("workspaces.json").exists(),
"a workspace was recorded as being looked after by nothing"
);
}
#[test]
fn run_is_untouched_and_is_still_the_answer_here() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
let planned = repository.ralon(&["run", "--dry-run", "--", "sh", "-c", "true"]);
assert_eq!(code(&planned), 0, "{}", stderr(&planned));
assert!(
stdout(&planned).contains("read-only"),
"{}",
stdout(&planned)
);
}
}
#[cfg(any(windows, target_os = "macos"))]
mod with_a_supervisor {
use super::*;
#[test]
fn a_repository_with_a_policy_becomes_enforced_with_nothing_run_inside_it() {
let machine = Machine::new();
let repository = machine.repository("app", None);
machine.tick();
assert!(
repository.writable(".env"),
"a project with no policy should be writable"
);
repository.declare(POLICY);
machine.tick();
assert!(
!repository.writable(".env"),
".env was still writable after the policy appeared"
);
assert!(
!repository.writable("src/index.tsx"),
"src/index.tsx was still writable"
);
assert!(
!repository.writable("config/db.yaml"),
"a file inside a protected directory was still writable"
);
assert!(
!repository.writable("agent.lock"),
"the policy could rewrite itself"
);
assert!(
repository.writable("src/App.tsx"),
"an unprotected file stopped being writable"
);
}
#[test]
fn an_enforced_project_is_given_the_agent_hook() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
let settings = repository.contents(".claude/settings.json");
assert!(
settings.contains("ralon hook check"),
"no hook was installed: {settings}"
);
let request = format!(
r#"{{"tool_name":"Update","tool_input":{{"file_path":"{}"}}}}"#,
repository
.path(".env")
.display()
.to_string()
.replace('\\', "\\\\")
);
let refused = repository.hook(&request);
assert_eq!(code(&refused), 2, "the hook allowed a protected write");
let said = stdout(&refused);
assert!(said.contains("protected by Ralon"), "{said}");
assert!(
said.contains("github.com/stoneware-dev/Ralon"),
"the refusal carries no link back to the project: {said}"
);
assert!(
!said.contains("EBUSY"),
"the refusal repeats the raw OS error it exists to replace: {said}"
);
}
#[test]
fn no_hooks_leaves_the_project_alone_but_still_enforces_it() {
let machine = Machine::new();
machine.set_hooks(false);
let repository = machine.repository("app", Some(POLICY));
machine.tick();
assert!(
!repository.path(".claude").exists(),
"the supervisor wrote agent configuration after being told not to"
);
assert!(
!repository.writable(".env"),
"enforcement was skipped along with the hooks"
);
}
#[test]
fn adding_a_scope_enforces_the_policies_already_under_it() {
let machine = Machine::new();
let elsewhere = machine.elsewhere("side");
let repository = machine.repository_in(&elsewhere, "app", Some(POLICY));
machine.tick();
assert!(
repository.writable(".env"),
"a project outside every scope was enforced anyway"
);
let added = machine.ralon(&["scope", "add", elsewhere.to_str().unwrap()]);
assert_eq!(code(&added), 0, "{}", stderr(&added));
assert!(
!repository.writable(".env"),
"`scope add` returned before enforcing what it had just taken on"
);
}
#[test]
fn removing_a_scope_releases_its_projects() {
let machine = Machine::new();
let elsewhere = machine.elsewhere("side");
let repository = machine.repository_in(&elsewhere, "app", Some(POLICY));
machine.ralon(&["scope", "add", elsewhere.to_str().unwrap()]);
assert!(!repository.writable(".env"));
let removed = machine.ralon(&["scope", "remove", elsewhere.to_str().unwrap()]);
assert_eq!(code(&removed), 0, "{}", stderr(&removed));
assert!(
repository.writable(".env"),
"dropping a scope left its projects locked with nothing watching them"
);
assert!(
!machine.recorded().contains("side"),
"a released project is still recorded: {}",
machine.recorded()
);
}
#[test]
fn several_scopes_are_enforced_at_once_and_independently() {
let machine = Machine::new();
let one = machine.elsewhere("one");
let two = machine.elsewhere("two");
let first = machine.repository_in(&one, "app", Some(POLICY));
let second = machine.repository_in(&two, "app", Some(POLICY));
machine.ralon(&["scope", "add", one.to_str().unwrap()]);
machine.ralon(&["scope", "add", two.to_str().unwrap()]);
assert!(!first.writable(".env"), "scope one was not enforced");
assert!(!second.writable(".env"), "scope two was not enforced");
machine.ralon(&["scope", "remove", one.to_str().unwrap()]);
assert!(first.writable(".env"), "scope one was not released");
assert!(
!second.writable(".env"),
"removing one scope released another"
);
}
#[test]
fn a_project_outside_every_scope_is_told_so_rather_than_ignored() {
let machine = Machine::new();
let elsewhere = machine.elsewhere("side");
let repository = machine.repository_in(&elsewhere, "app", Some(POLICY));
machine.tick();
let status = repository.ralon(&["status"]);
let said = stdout(&status);
assert!(said.contains("outside every scope"), "{said}");
assert!(said.contains("NOT protected"), "{said}");
assert!(
said.contains("ralon scope add"),
"the fix was not offered: {said}"
);
}
#[test]
fn scopes_fold_rather_than_overlapping() {
let machine = Machine::new();
let outer = machine.elsewhere("outer");
let inner = outer.join("inner");
fs::create_dir_all(&inner).unwrap();
machine.ralon(&["scope", "add", inner.to_str().unwrap()]);
let broader = machine.ralon(&["scope", "add", outer.to_str().unwrap()]);
assert!(
stdout(&broader).contains("absorbed"),
"{}",
stdout(&broader)
);
let listed = stdout(&machine.ralon(&["scope", "list"]));
assert!(listed.contains("outer"), "{listed}");
assert_eq!(
listed.matches("inner").count(),
0,
"the absorbed scope is still listed: {listed}"
);
let again = machine.ralon(&["scope", "add", inner.to_str().unwrap()]);
assert!(
stdout(&again).contains("already covered"),
"{}",
stdout(&again)
);
}
#[test]
fn equivalent_spellings_of_a_path_are_one_scope() {
let machine = Machine::new();
let elsewhere = machine.elsewhere("side");
machine.ralon(&["scope", "add", elsewhere.to_str().unwrap()]);
let indirect = elsewhere.join("..").join(
elsewhere
.file_name()
.expect("the scope has a final component"),
);
let again = machine.ralon(&["scope", "add", indirect.to_str().unwrap()]);
assert!(
stdout(&again).contains("already covered"),
"`{}` was treated as a different directory: {}",
indirect.display(),
stdout(&again)
);
#[cfg(windows)]
{
let shouted = elsewhere.to_str().unwrap().to_uppercase();
let cased = machine.ralon(&["scope", "add", &shouted]);
assert!(
stdout(&cased).contains("already covered"),
"casing made a second scope on a case-insensitive filesystem: {}",
stdout(&cased)
);
}
}
#[test]
fn removing_a_directory_inside_a_scope_is_refused_with_a_reason() {
let machine = Machine::new();
let elsewhere = machine.elsewhere("side");
let inner = elsewhere.join("app");
fs::create_dir_all(&inner).unwrap();
machine.ralon(&["scope", "add", elsewhere.to_str().unwrap()]);
let attempt = machine.ralon(&["scope", "remove", inner.to_str().unwrap()]);
assert_ne!(code(&attempt), 0, "a non-scope was reported as removed");
assert!(
stderr(&attempt).contains("is inside"),
"{}",
stderr(&attempt)
);
assert!(
stdout(&machine.ralon(&["scope", "list"])).contains("side"),
"the scope was dropped anyway"
);
}
#[test]
fn a_scope_whose_directory_disappears_does_not_disturb_the_others() {
let machine = Machine::new();
let removable = machine.elsewhere("removable");
machine.ralon(&["scope", "add", removable.to_str().unwrap()]);
let still_here = machine.repository("local", Some(POLICY));
machine.tick();
assert!(!still_here.writable(".env"));
fs::remove_dir_all(&removable).unwrap();
let tick = machine.tick();
assert_eq!(code(&tick), 0, "{}", stderr(&tick));
assert!(
!still_here.writable(".env"),
"an unreachable scope released a project under a scope that is fine"
);
let listed = stdout(&machine.ralon(&["scope", "list"]));
assert!(listed.contains("unreachable"), "{listed}");
}
#[test]
#[cfg(windows)]
fn an_ancestor_cannot_be_renamed_and_substituted() {
let machine = Machine::new();
let repository = machine.repository_in(
&machine.code.clone(),
"nested",
Some("version: 1\nprotect:\n - src/deep/secret.txt\n"),
);
fs::create_dir_all(repository.path("src/deep")).unwrap();
fs::write(repository.path("src/deep/secret.txt"), "ORIGINAL").unwrap();
machine.tick();
for attack in [
"ren src\\deep moved && mkdir src\\deep && echo PWNED> src\\deep\\secret.txt",
"ren src src2 && mkdir src\\deep && echo PWNED> src\\deep\\secret.txt",
"mkdir decoy && echo PWNED> decoy\\secret.txt && move /y src\\deep old && move /y decoy src\\deep",
"rmdir /s /q src\\deep && mkdir src\\deep && echo PWNED> src\\deep\\secret.txt",
"mkdir evil && echo PWNED> evil\\secret.txt && rmdir /s /q src\\deep && mklink /J src\\deep evil",
"move /y src\\deep ..\\stolen",
] {
shell(&repository.root, attack);
assert_eq!(
repository.contents("src/deep/secret.txt"),
"ORIGINAL",
"`{attack}` put different content at the protected path"
);
}
}
#[test]
fn a_protected_directory_refuses_new_entries() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
assert!(
!repository.creatable("config/slipped-in.yaml"),
"a new file was created inside a protected directory"
);
assert!(
repository.creatable("src/allowed.tsx"),
"a new file could not be created in an unprotected directory"
);
}
#[test]
fn removing_the_policy_releases_the_workspace() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
assert!(!repository.writable(".env"));
assert!(
repository
.ralon(&["pause", "--indefinitely"])
.status
.success(),
"pause failed"
);
repository.undeclare();
machine.tick();
assert!(
repository.writable(".env"),
"the workspace stayed enforced after its policy was deleted"
);
assert!(
!machine.recorded().contains("\"app\""),
"a workspace with no policy is still recorded: {}",
machine.recorded()
);
}
#[test]
fn a_policy_deleted_while_the_supervisor_was_down_is_still_released() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
repository.ralon(&["guard", "--stop"]);
repository.undeclare();
let tick = machine.tick();
assert_eq!(code(&tick), 0, "{}", stderr(&tick));
assert!(
!machine.recorded().contains("app"),
"a deleted workspace was never cleaned up: {}",
machine.recorded()
);
assert!(repository.writable(".env"), ".env stayed locked");
}
#[test]
fn two_repositories_are_enforced_at_once_and_do_not_interfere() {
let machine = Machine::new();
let first = machine.repository("one", Some(POLICY));
let second = machine.repository("two", Some("version: 1\nprotect:\n - src/index.tsx\n"));
machine.tick();
assert!(!first.writable(".env"), "one/.env was writable");
assert!(!first.writable("src/index.tsx"), "one/src/index.tsx");
assert!(!second.writable("src/index.tsx"), "two/src/index.tsx");
assert!(
second.writable(".env"),
"two/.env is not in two's policy and must stay writable"
);
assert!(second.ralon(&["pause", "--indefinitely"]).status.success());
assert!(second.writable("src/index.tsx"), "two was not released");
assert!(
!first.writable("src/index.tsx"),
"pausing one repository released another"
);
}
#[test]
fn a_repository_cloned_after_setup_is_picked_up() {
let machine = Machine::new();
let _existing = machine.repository("old", Some(POLICY));
machine.tick();
let cloned = machine.repository("cloned-later", Some(POLICY));
machine.tick();
assert!(
!cloned.writable(".env"),
"a repository cloned after install was not protected"
);
}
#[test]
fn restarting_the_supervisor_changes_nothing() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
let after_first = machine.recorded();
let second = machine.tick();
let third = machine.tick();
assert_eq!(code(&second), 0, "{}", stderr(&second));
assert!(
stdout(&third).contains("nothing to change"),
"a restart re-did work that was already done: {}",
stdout(&third)
);
assert_eq!(
after_first,
machine.recorded(),
"the recorded state drifted across restarts"
);
assert!(
!repository.writable(".env"),
"enforcement lapsed on restart"
);
}
#[test]
fn enforcement_is_restored_after_the_machine_restarts() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
assert!(!repository.writable(".env"));
repository.ralon(&["guard", "--stop"]);
assert!(
repository.writable(".env"),
"the simulated reboot did not actually release anything, \
so this test would pass without proving anything"
);
machine.tick();
assert!(
!repository.writable(".env"),
"enforcement was not restored after a restart"
);
}
#[test]
fn enforcement_holds_against_many_agents_writing_at_once() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
let root = repository.root.clone();
let workers: Vec<_> = (0..12)
.map(|index| {
let root = root.clone();
std::thread::spawn(move || {
for _ in 0..5 {
for target in [".env", "src/index.tsx", "agent.lock"] {
shell(
&root,
&redirect(&format!("AGENT{index}"), &root.join(target)),
);
}
}
})
})
.collect();
for worker in workers {
worker.join().unwrap();
}
assert_eq!(repository.contents(".env"), "SECRET=original");
assert_eq!(repository.contents("src/index.tsx"), "original");
assert_eq!(repository.contents("agent.lock"), POLICY);
}
#[test]
fn enforcement_holds_however_the_write_is_attempted() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
let env = repository.path(".env");
let index = repository.path("src/index.tsx");
let decoy = repository.path("src/decoy.tsx");
fs::write(&decoy, "DECOY").unwrap();
#[cfg(windows)]
let attacks = vec![
format!("echo X> \"{}\"", env.display()),
format!("del /f /q \"{}\"", env.display()),
format!("move /y \"{}\" \"{}.bak\"", env.display(), env.display()),
format!("move /y \"{}\" \"{}\"", decoy.display(), index.display()),
format!("type \"{}\" > \"{}\"", decoy.display(), env.display()),
];
#[cfg(not(windows))]
let attacks = vec![
format!("echo X> '{}'", env.display()),
format!("rm -f '{}'", env.display()),
format!("mv '{}' '{}.bak'", env.display(), env.display()),
format!("mv '{}' '{}'", decoy.display(), index.display()),
format!("cat '{}' > '{}'", decoy.display(), env.display()),
];
for attack in &attacks {
shell(&repository.root, attack);
assert_eq!(
repository.contents(".env"),
"SECRET=original",
"`{attack}` got through to .env"
);
assert_eq!(
repository.contents("src/index.tsx"),
"original",
"`{attack}` got through to src/index.tsx"
);
}
#[cfg(windows)]
let (script, runner) = ("attack.cmd", "cmd");
#[cfg(not(windows))]
let (script, runner) = ("attack.sh", "sh");
let script = repository.root.join(script);
fs::write(&script, attacks.join("\n")).unwrap();
let mut process = Command::new(runner);
#[cfg(windows)]
process.arg("/c");
process.arg(&script);
let _ = process.current_dir(&repository.root).output();
assert_eq!(repository.contents(".env"), "SECRET=original");
assert_eq!(repository.contents("src/index.tsx"), "original");
}
#[test]
fn pause_hands_the_policy_back_and_resume_takes_it_again() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
assert!(!repository.writable("agent.lock"));
let paused = repository.ralon(&["pause", "--indefinitely"]);
assert_eq!(code(&paused), 0, "{}", stderr(&paused));
assert!(
repository.writable("agent.lock"),
"pause returned before releasing the policy file"
);
let status = repository.ralon(&["status"]);
assert!(
stdout(&status).contains("PAUSED"),
"a paused workspace did not say so: {}",
stdout(&status)
);
let resumed = repository.ralon(&["resume"]);
assert_eq!(code(&resumed), 0, "{}", stderr(&resumed));
assert!(
!repository.writable("agent.lock"),
"resume reported success without restoring enforcement"
);
}
#[test]
fn a_paused_workspace_is_not_re_enforced_until_it_expires() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
repository.ralon(&["pause", "--indefinitely"]);
machine.tick();
assert!(
repository.writable("agent.lock"),
"a tick re-enforced a paused workspace"
);
let recorded = machine
.recorded()
.replace(r#""until": null"#, r#""until": 1"#);
assert!(
recorded.contains(r#""until": 1"#),
"the deadline was never rewritten, so this test proves nothing: {recorded}"
);
fs::write(machine.home.join("workspaces.json"), recorded).unwrap();
machine.tick();
assert!(
!repository.writable("agent.lock"),
"a pause that had run out was not taken back"
);
}
#[test]
fn status_reports_the_supervisor_and_the_workspace_separately() {
let machine = Machine::new();
let repository = machine.repository("app", Some(POLICY));
machine.tick();
let status = repository.ralon(&["status"]);
let said = stdout(&status);
assert!(said.contains("supervisor"), "{said}");
assert!(said.contains("enforced by the supervisor"), "{said}");
}
}