use crate::VmError;
use super::harness::*;
#[cfg(target_os = "macos")]
#[test]
fn test_macos_process_sandbox_surfaces_denial_as_typed_error() {
if !std::path::Path::new("/usr/bin/sandbox-exec").exists() {
return;
}
let cwd = std::env::current_dir().unwrap();
let allowed = tempfile::tempdir_in(&cwd).unwrap();
let outside_base = std::env::var_os("HOME")
.map(std::path::PathBuf::from)
.filter(|path| path.is_dir())
.unwrap_or_else(|| cwd.parent().unwrap_or(cwd.as_path()).to_path_buf());
if outside_base.starts_with("/tmp") || outside_base.starts_with("/private/tmp") {
return;
}
let outside = tempfile::tempdir_in(outside_base).unwrap();
let outside_file = outside.path().join("blocked.txt");
let sandbox_env = crate::stdlib::sandbox::handler_sandbox_test_guard();
sandbox_env.set("enforce");
let policy = crate::orchestration::CapabilityPolicy {
capabilities: std::collections::BTreeMap::from([(
"process".to_string(),
vec!["exec".to_string()],
)]),
workspace_roots: vec![allowed.path().display().to_string()],
side_effect_level: Some("process_exec".to_string()),
..Default::default()
};
let source = format!(
r#"pipeline t(task) {{ shell("printf denied > '{}'") }}"#,
outside_file.display()
);
let err = run_harn_with_policy(&source, policy).unwrap_err();
assert!(matches!(
err,
VmError::CategorizedError {
category: crate::value::ErrorCategory::ToolRejected,
..
}
));
assert!(err.to_string().contains("sandbox violation"));
assert!(!outside_file.exists());
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_process_sandbox_catches_ten_process_escapes() {
let allowed = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let outside_file = outside.path().join("secret.txt");
let outside_new = outside.path().join("new.txt");
let outside_copy = outside.path().join("copy.txt");
let outside_dir = outside.path().join("new_dir");
let allowed_file = allowed.path().join("allowed.txt");
std::fs::write(&outside_file, "secret").unwrap();
std::fs::write(&allowed_file, "allowed").unwrap();
let sandbox_env = crate::stdlib::sandbox::handler_sandbox_test_guard();
sandbox_env.set("enforce");
let policy = crate::orchestration::CapabilityPolicy {
capabilities: std::collections::BTreeMap::from([
("process".to_string(), vec!["exec".to_string()]),
(
"workspace".to_string(),
vec![
"read_text".to_string(),
"list".to_string(),
"exists".to_string(),
"write_text".to_string(),
"delete".to_string(),
],
),
]),
workspace_roots: vec![allowed.path().display().to_string()],
side_effect_level: Some("process_exec".to_string()),
..Default::default()
};
let escapes = [
format!("cat {}", shell_quote(&outside_file)),
format!("printf x > {}", shell_quote(&outside_new)),
format!("printf x >> {}", shell_quote(&outside_file)),
format!("mkdir {}", shell_quote(&outside_dir)),
format!("rm {}", shell_quote(&outside_file)),
format!(
"cp {} {}",
shell_quote(&outside_file),
shell_quote(&allowed.path().join("copy.txt"))
),
format!(
"cp {} {}",
shell_quote(&allowed_file),
shell_quote(&outside_copy)
),
format!(
"mv {} {}",
shell_quote(&allowed_file),
shell_quote(&outside.path().join("moved.txt"))
),
format!(
"ln -s {} {} && cat {}",
shell_quote(&outside_file),
shell_quote(&allowed.path().join("link.txt")),
shell_quote(&allowed.path().join("link.txt"))
),
format!("touch {}", shell_quote(&outside.path().join("touched.txt"))),
];
assert_eq!(escapes.len(), 10);
for command in escapes {
let source = format!(
r#"pipeline t(task) {{ shell("{}") }}"#,
harn_string_escape(&command)
);
let err = run_harn_with_policy(&source, policy.clone()).unwrap_err();
assert!(
matches!(
err,
VmError::CategorizedError {
category: crate::value::ErrorCategory::ToolRejected,
..
}
),
"expected tool_rejected for command {command}, got {err:?}"
);
assert!(
err.to_string().contains("sandbox violation"),
"expected sandbox violation for command {command}, got {err}"
);
}
assert!(outside_file.exists());
assert!(!outside_new.exists());
assert!(!outside_copy.exists());
assert!(!outside_dir.exists());
}
#[cfg(target_os = "windows")]
#[test]
fn test_windows_process_sandbox_allows_process_exec_in_workspace() {
let allowed = tempfile::tempdir().unwrap();
let allowed_file = allowed.path().join("allowed.txt");
let sandbox_env = crate::stdlib::sandbox::handler_sandbox_test_guard();
sandbox_env.set("enforce");
let policy = crate::orchestration::CapabilityPolicy {
capabilities: std::collections::BTreeMap::from([
("process".to_string(), vec!["exec".to_string()]),
("workspace".to_string(), vec!["write_text".to_string()]),
]),
workspace_roots: vec![allowed.path().display().to_string()],
side_effect_level: Some("process_exec".to_string()),
..Default::default()
};
let command = format!("echo allowed>{}", allowed_file.display());
let source = format!(
r#"pipeline t(task) {{ shell("{}") }}"#,
harn_string_escape(&command)
);
let result = run_harn_with_policy(&source, policy);
result.unwrap();
assert!(allowed_file.exists());
}
#[cfg(target_os = "windows")]
#[test]
fn test_windows_process_sandbox_allows_exec_argv0() {
let allowed = tempfile::tempdir().unwrap();
let sandbox_env = crate::stdlib::sandbox::handler_sandbox_test_guard();
sandbox_env.set("enforce");
let policy = crate::orchestration::CapabilityPolicy {
capabilities: std::collections::BTreeMap::from([(
"process".to_string(),
vec!["exec".to_string()],
)]),
workspace_roots: vec![allowed.path().display().to_string()],
side_effect_level: Some("process_exec".to_string()),
..Default::default()
};
let result = run_harn_with_policy(
r#"pipeline t(task) { exec("cmd", "/C", "exit 0") }"#,
policy,
);
result.unwrap();
}
#[cfg(target_os = "windows")]
#[test]
fn test_windows_process_sandbox_denies_write_outside_workspace() {
let allowed = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let outside_file = outside.path().join("blocked.txt");
let sandbox_env = crate::stdlib::sandbox::handler_sandbox_test_guard();
sandbox_env.set("enforce");
let policy = crate::orchestration::CapabilityPolicy {
capabilities: std::collections::BTreeMap::from([
("process".to_string(), vec!["exec".to_string()]),
("workspace".to_string(), vec!["write_text".to_string()]),
]),
workspace_roots: vec![allowed.path().display().to_string()],
side_effect_level: Some("process_exec".to_string()),
..Default::default()
};
let command = format!("echo denied>{}", outside_file.display());
let source = format!(
r#"pipeline t(task) {{ shell("{}") }}"#,
harn_string_escape(&command)
);
let err = run_harn_with_policy(&source, policy).unwrap_err();
assert!(matches!(
err,
VmError::CategorizedError {
category: crate::value::ErrorCategory::ToolRejected,
..
}
));
assert!(
err.to_string().contains("sandbox violation")
|| err.to_string().contains("process sandbox failed"),
"expected sandbox denial, got {err}"
);
assert!(!outside_file.exists());
}
#[cfg(target_os = "linux")]
fn shell_quote(path: &std::path::Path) -> String {
shell_words::quote(&path.to_string_lossy()).into_owned()
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn harn_string_escape(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}