#![allow(rustdoc::private_intra_doc_links)]
use anyhow::Result;
use koda_sandbox::{
SandboxPolicy, SandboxRuntime, SandboxTransformRequest, ca_bundle_for_policy, current_runtime,
is_available as ks_is_available, proxy_env_vars,
};
use std::path::Path;
use std::sync::OnceLock;
use tokio::process::Command;
pub fn is_available() -> bool {
ks_is_available()
}
pub(crate) fn is_fully_denied(path: &Path) -> bool {
koda_sandbox::is_fully_denied(path)
}
pub fn build(
command: &str,
project_root: &Path,
trust: &crate::trust::TrustMode,
policy: &SandboxPolicy,
proxy_port: Option<u16>,
socks5_port: Option<u16>,
) -> Result<Command> {
let runtime = current_runtime();
warn_if_unavailable_once(runtime.as_ref());
if matches!(trust, crate::trust::TrustMode::Auto) && !is_available() {
anyhow::bail!(
"Kernel sandbox backend unavailable in Auto mode \u{2014} refusing to run \
unsandboxed. Install the platform sandbox dependency (e.g. `bwrap` on \
Linux) or switch to Safe/Plan mode (which keeps the user in the \
approval loop)."
);
}
let req = SandboxTransformRequest {
command,
project_root,
policy,
proxy_port,
};
let mut cmd = match runtime.transform(req) {
Ok(exec) => exec.command,
Err(e) => {
tracing::warn!("Sandbox transform failed, running unsandboxed: {e}");
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(command).current_dir(project_root);
cmd
}
};
koda_sandbox::rlimits::apply_to_command(&mut cmd, &policy.limits);
if let Some(port) = proxy_port {
let ca = ca_bundle_for_policy(&policy.net);
for (k, v) in proxy_env_vars(port, ca) {
cmd.env(k, v);
}
}
if let Some(port) = socks5_port {
for (k, v) in koda_sandbox::socks5_env_vars(port) {
cmd.env(k, v);
}
}
Ok(cmd)
}
pub fn policy_for_agent(trust: crate::trust::TrustMode, project_root: &Path) -> SandboxPolicy {
let mut policy = SandboxPolicy::strict_default();
policy.limits.wall_time_secs = Some(match trust {
crate::trust::TrustMode::Plan
| crate::trust::TrustMode::Safe
| crate::trust::TrustMode::Auto => 60,
});
policy.fs.mandatory_deny_search_depth = match trust {
crate::trust::TrustMode::Plan => 3, crate::trust::TrustMode::Safe => 5, crate::trust::TrustMode::Auto => 10, };
let canonical_root = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
policy.fs.allow_write = vec![canonical_root.clone().into()];
if !policy.fs.allow_git_config {
policy.fs.deny_write_within_allow = vec![
canonical_root.join(".git/hooks").into(),
canonical_root.join(".git/config").into(),
];
}
policy
}
pub fn compose_child_policy(
parent: &SandboxPolicy,
sub_trust: crate::trust::TrustMode,
project_root: &Path,
) -> SandboxPolicy {
let child = policy_for_agent(sub_trust, project_root);
SandboxPolicy::compose(parent, &child)
}
fn warn_if_unavailable_once(runtime: &dyn SandboxRuntime) {
static WARNED: OnceLock<()> = OnceLock::new();
WARNED.get_or_init(|| {
let report = runtime.check_dependencies();
if !report.available {
tracing::warn!(
"Sandbox backend {:?} unavailable — commands run unsandboxed. {}",
report.backend,
report.reason.as_deref().unwrap_or("")
);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn build_attaches_proxy_env_when_port_set() {
let dir = tempfile::tempdir().unwrap();
let out = build(
"echo \"$HTTPS_PROXY\"",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
Some(31415),
None,
)
.unwrap()
.output()
.await
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("http://127.0.0.1:31415"),
"HTTPS_PROXY must be set, got stdout={stdout:?}"
);
}
#[tokio::test]
async fn build_omits_proxy_env_when_port_none() {
let dir = tempfile::tempdir().unwrap();
let out = build(
"echo \"[$HTTPS_PROXY]\"",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.output()
.await
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("[]"),
"HTTPS_PROXY must be unset, got stdout={stdout:?}"
);
}
#[tokio::test]
async fn build_attaches_socks5_env_when_port_set() {
let dir = tempfile::tempdir().unwrap();
let out = build(
"echo \"$ALL_PROXY|$all_proxy\"",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
Some(27182),
)
.unwrap()
.output()
.await
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("socks5h://127.0.0.1:27182|socks5h://127.0.0.1:27182"),
"ALL_PROXY/all_proxy must be set, got stdout={stdout:?}"
);
}
#[tokio::test]
async fn build_omits_socks5_env_when_port_none() {
let dir = tempfile::tempdir().unwrap();
let out = build(
"echo \"[$ALL_PROXY]\"",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.output()
.await
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("[]"),
"ALL_PROXY must be unset, got stdout={stdout:?}"
);
}
#[tokio::test]
async fn build_attaches_both_proxy_and_socks5_when_both_set() {
let dir = tempfile::tempdir().unwrap();
let out = build(
"echo \"$HTTPS_PROXY|$ALL_PROXY\"",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
Some(8080),
Some(1080),
)
.unwrap()
.output()
.await
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("http://127.0.0.1:8080|socks5h://127.0.0.1:1080"),
"both vars must be set with their distinct schemes, got stdout={stdout:?}"
);
}
#[tokio::test]
async fn build_attaches_all_proxy_var_keys() {
let dir = tempfile::tempdir().unwrap();
let cmd = r#"
for v in HTTPS_PROXY https_proxy HTTP_PROXY http_proxy NO_PROXY no_proxy; do
eval "echo $v=\$$v"
done
"#;
let out = build(
cmd,
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
Some(8080),
None,
)
.unwrap()
.output()
.await
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
for v in ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] {
assert!(
stdout.contains(&format!("{v}=http://127.0.0.1:8080")),
"{v} missing from child env, got: {stdout:?}"
);
}
assert!(stdout.contains("NO_PROXY="), "NO_PROXY missing: {stdout:?}");
assert!(stdout.contains("no_proxy="), "no_proxy missing: {stdout:?}");
}
#[tokio::test]
async fn build_always_succeeds_and_runs_echo() {
let dir = tempfile::tempdir().unwrap();
let status = build(
"echo ok",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(status.success());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_allows_write_inside_project() {
let dir = tempfile::tempdir().unwrap();
let status = build(
"touch sandbox_canary",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(status.success(), "write inside project must succeed");
assert!(dir.path().join("sandbox_canary").exists());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_blocks_write_outside_project() {
let project = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let target = outside.path().join("evil.txt");
let status = build(
&format!("echo pwned > {}", target.display()),
project.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(!status.success(), "write outside project must be blocked");
assert!(!target.exists(), "file must not have been created");
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_allows_read_outside_project() {
let dir = tempfile::tempdir().unwrap();
let status = build(
"cat /etc/hosts > /dev/null",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(status.success(), "reads outside project must be allowed");
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_allows_write_inside_project() {
let dir = tempfile::tempdir().unwrap();
let status = build(
"touch strict_canary",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
status.success(),
"strict: writes inside project must succeed"
);
assert!(dir.path().join("strict_canary").exists());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_blocks_write_outside_project() {
let project = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let target = outside.path().join("evil.txt");
let status = build(
&format!("echo pwned > {}", target.display()),
project.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(!status.success(), "write outside project must be blocked");
assert!(!target.exists(), "file must not have been created");
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_allows_reads_outside_sensitive() {
let dir = tempfile::tempdir().unwrap();
let status = build(
"cat /etc/hosts > /dev/null",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
status.success(),
"strict: reads to /etc/hosts must still be allowed"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_blocks_koda_db_read() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let db_dir = format!("{home}/.config/koda/db");
if !Path::new(&db_dir).exists() {
eprintln!("skip: {db_dir} does not exist");
return;
}
let dir = tempfile::tempdir().unwrap();
let status = build(
&format!("ls {db_dir}"),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
!status.success(),
"strict: reading ~/.config/koda/db/ must be blocked"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_allows_ssh_read() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let ssh_dir = format!("{home}/.ssh");
if !Path::new(&ssh_dir).exists() {
eprintln!("skip: {ssh_dir} does not exist");
return;
}
let dir = tempfile::tempdir().unwrap();
let status = build(
&format!("ls {ssh_dir}"),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
status.success(),
"strict: reading ~/.ssh/ must be allowed (CLI tools need credential access, #855)"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_blocks_ssh_write() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/test".into());
let ssh_dir = format!("{home}/.ssh");
if !Path::new(&ssh_dir).exists() {
eprintln!("skip: {ssh_dir} does not exist");
return;
}
let dir = tempfile::tempdir().unwrap();
let canary = format!("{ssh_dir}/sandbox_canary_test");
let status = build(
&format!("touch {canary}"),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
!status.success(),
"strict: writing to ~/.ssh/ must be blocked"
);
assert!(!Path::new(&canary).exists());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_project_blocks_write_to_koda_agents() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".koda/agents")).unwrap();
let target = dir.path().join(".koda/agents/evil.json");
let status = build(
&format!("echo '{{}}' > {}", target.display()),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
!status.success(),
"project: writes to .koda/agents/ must be blocked"
);
assert!(!target.exists(), "agent file must not have been created");
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_strict_blocks_write_to_koda_agents() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".koda/agents")).unwrap();
let target = dir.path().join(".koda/agents/evil.json");
let status = build(
&format!("echo '{{}}' > {}", target.display()),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
!status.success(),
"strict: writes to .koda/agents/ must be blocked"
);
assert!(!target.exists());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_project_allows_normal_writes_with_agents_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".koda/agents")).unwrap();
let status = build(
"touch normal_file.txt",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
status.success(),
"project: normal writes must still work alongside agent protection"
);
assert!(dir.path().join("normal_file.txt").exists());
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn macos_project_blocks_write_to_koda_skills() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".koda/skills")).unwrap();
let target = dir.path().join(".koda/skills/evil.md");
let status = build(
&format!("echo '# evil' > {}", target.display()),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
!status.success(),
"project: writes to .koda/skills/ must be blocked"
);
assert!(!target.exists(), "skill file must not have been created");
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn linux_strict_allows_ssh_read() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
let ssh_dir = format!("{home}/.ssh");
if !Path::new(&ssh_dir).exists() {
eprintln!("skip: {ssh_dir} does not exist");
return;
}
let dir = tempfile::tempdir().unwrap();
let status = build(
&format!("ls {ssh_dir}"),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
status.success(),
"linux strict: reading ~/.ssh/ must be allowed (CLI tools need credential access, #855)"
);
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn linux_strict_blocks_ssh_write() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
let ssh_dir = format!("{home}/.ssh");
if !Path::new(&ssh_dir).exists() {
eprintln!("skip: {ssh_dir} does not exist");
return;
}
let dir = tempfile::tempdir().unwrap();
let canary = format!("{ssh_dir}/bwrap_canary_test");
let status = build(
&format!("touch {canary}"),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
!status.success(),
"linux strict: writing to ~/.ssh/ must be blocked"
);
assert!(!Path::new(&canary).exists());
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn linux_strict_allows_aws_read() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
let aws_dir = format!("{home}/.aws");
if !Path::new(&aws_dir).exists() {
eprintln!("skip: {aws_dir} does not exist");
return;
}
let dir = tempfile::tempdir().unwrap();
let status = build(
&format!("ls {aws_dir}"),
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
)
.unwrap()
.status()
.await
.unwrap();
assert!(
status.success(),
"linux strict: reading ~/.aws/ must be allowed (aws CLI needs credentials)"
);
}
#[tokio::test]
async fn build_errors_in_auto_mode_when_sandbox_unavailable() {
if is_available() {
eprintln!("skip: kernel sandbox is available on this host");
return;
}
let dir = tempfile::tempdir().unwrap();
let result = build(
"echo hi",
dir.path(),
&crate::trust::TrustMode::Auto,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
);
assert!(
result.is_err(),
"Auto mode without kernel sandbox must hard-error \u{2014} the whole \
point of Auto is the kernel boundary"
);
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("Auto"),
"error message must name the offending mode so the user knows what to change: {err}"
);
}
#[tokio::test]
async fn build_falls_back_in_safe_mode_when_sandbox_unavailable() {
if is_available() {
eprintln!("skip: kernel sandbox is available on this host");
return;
}
let dir = tempfile::tempdir().unwrap();
let result = build(
"echo hi",
dir.path(),
&crate::trust::TrustMode::Safe,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
);
assert!(
result.is_ok(),
"Safe mode must keep the warn-and-fallback path \u{2014} the user is \
already in the approval loop: {result:?}"
);
}
#[tokio::test]
async fn build_falls_back_in_plan_mode_when_sandbox_unavailable() {
if is_available() {
eprintln!("skip: kernel sandbox is available on this host");
return;
}
let dir = tempfile::tempdir().unwrap();
let result = build(
"echo hi",
dir.path(),
&crate::trust::TrustMode::Plan,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
);
assert!(
result.is_ok(),
"Plan mode must keep the warn-and-fallback path \u{2014} the tool registry \
filters writes already: {result:?}"
);
}
#[tokio::test]
async fn build_succeeds_in_all_modes_when_sandbox_available() {
if !is_available() {
eprintln!("skip: kernel sandbox not available on this host");
return;
}
let dir = tempfile::tempdir().unwrap();
for mode in [
crate::trust::TrustMode::Plan,
crate::trust::TrustMode::Safe,
crate::trust::TrustMode::Auto,
] {
let result = build(
"echo hi",
dir.path(),
&mode,
&koda_sandbox::SandboxPolicy::strict_default(),
None,
None,
);
assert!(
result.is_ok(),
"{mode:?} must succeed when sandbox is available: {result:?}"
);
}
}
#[cfg(unix)]
#[tokio::test]
async fn build_applies_resource_limits_to_spawned_command() {
let dir = tempfile::tempdir().unwrap();
let mut policy = koda_sandbox::SandboxPolicy::strict_default();
policy.limits.max_open_fds = Some(64);
let mut cmd = build(
"ulimit -n",
dir.path(),
&crate::trust::TrustMode::Safe,
&policy,
None,
None,
)
.expect("build must succeed (Safe mode falls back if sandbox unavailable)");
let out = cmd.output().await.expect("spawn ok");
assert!(out.status.success(), "child should succeed: {out:?}");
let reported: u64 = String::from_utf8_lossy(&out.stdout)
.trim()
.parse()
.expect("ulimit -n prints a number");
assert_eq!(
reported, 64,
"build() must apply policy.limits.max_open_fds to the child"
);
}
#[test]
fn policy_for_agent_sets_wall_time_for_all_trust_modes() {
let dir = tempfile::tempdir().unwrap();
for mode in [
crate::trust::TrustMode::Plan,
crate::trust::TrustMode::Safe,
crate::trust::TrustMode::Auto,
] {
let policy = policy_for_agent(mode, dir.path());
assert_eq!(
policy.limits.wall_time_secs,
Some(60),
"PR-3 contract: every trust mode ships with a wall_time default \
so the Bash dispatch path stops needing a hardcoded fallback. \
Mode under test: {mode:?}"
);
}
}
#[test]
fn policy_for_agent_leaves_other_limits_unlimited() {
let dir = tempfile::tempdir().unwrap();
let policy = policy_for_agent(crate::trust::TrustMode::Safe, dir.path());
assert_eq!(policy.limits.cpu_time_secs, None);
assert_eq!(policy.limits.max_rss_bytes, None);
assert_eq!(policy.limits.max_open_fds, None);
assert_eq!(policy.limits.max_output_bytes, None);
}
#[test]
fn policy_for_agent_does_not_panic_on_nonexistent_project_root() {
let _ = policy_for_agent(
crate::trust::TrustMode::Safe,
std::path::Path::new("/nonexistent/path/that/should/not/exist"),
);
}
#[test]
fn policy_for_agent_plan_mode_uses_shallow_depth() {
let dir = tempfile::tempdir().unwrap();
let policy = policy_for_agent(crate::trust::TrustMode::Plan, dir.path());
assert_eq!(policy.fs.mandatory_deny_search_depth, 3);
}
#[test]
fn policy_for_agent_safe_mode_uses_balanced_depth() {
let dir = tempfile::tempdir().unwrap();
let policy = policy_for_agent(crate::trust::TrustMode::Safe, dir.path());
assert_eq!(policy.fs.mandatory_deny_search_depth, 5);
}
#[test]
fn policy_for_agent_auto_mode_uses_max_depth() {
let dir = tempfile::tempdir().unwrap();
let policy = policy_for_agent(crate::trust::TrustMode::Auto, dir.path());
assert_eq!(
policy.fs.mandatory_deny_search_depth, 10,
"Auto mode runs without a user gate — deep deny-rule checking is the \
defense-in-depth that prevents creative path-evasion bypasses"
);
}
#[test]
fn policy_for_agent_depth_is_strictly_monotone_with_permissiveness() {
let dir = tempfile::tempdir().unwrap();
let plan = policy_for_agent(crate::trust::TrustMode::Plan, dir.path())
.fs
.mandatory_deny_search_depth;
let safe = policy_for_agent(crate::trust::TrustMode::Safe, dir.path())
.fs
.mandatory_deny_search_depth;
let auto = policy_for_agent(crate::trust::TrustMode::Auto, dir.path())
.fs
.mandatory_deny_search_depth;
assert!(
plan < safe && safe < auto,
"trust permissiveness must imply paranoia depth: \
Plan({plan}) < Safe({safe}) < Auto({auto})"
);
}
#[test]
fn compose_child_policy_with_strict_default_parent_is_just_child_policy() {
let dir = tempfile::tempdir().unwrap();
let parent = SandboxPolicy::strict_default();
let composed = compose_child_policy(&parent, crate::trust::TrustMode::Safe, dir.path());
let child_alone = policy_for_agent(crate::trust::TrustMode::Safe, dir.path());
assert_eq!(composed.fs.deny_read, child_alone.fs.deny_read);
assert_eq!(
composed.fs.mandatory_deny_search_depth,
child_alone.fs.mandatory_deny_search_depth
);
assert_eq!(
composed.fs.allow_git_config,
child_alone.fs.allow_git_config
);
assert_eq!(composed.limits, child_alone.limits);
assert_eq!(composed.trust, child_alone.trust);
assert!(
composed.fs.allow_write.is_empty(),
"parent-wins: strict_default's empty allow_write beats child's [root]; \
top-level agents should NOT go through compose (use policy_for_agent \
directly) — #1072 Gap 3 comment"
);
}
#[test]
fn policy_for_agent_seeds_allow_write_with_canonical_root() {
let dir = tempfile::tempdir().unwrap();
for mode in [
crate::trust::TrustMode::Plan,
crate::trust::TrustMode::Safe,
crate::trust::TrustMode::Auto,
] {
let policy = policy_for_agent(mode, dir.path());
assert_eq!(
policy.fs.allow_write.len(),
1,
"policy_for_agent must seed allow_write with the project root \
(one entry) for all trust modes, got {} entries for {mode:?}",
policy.fs.allow_write.len()
);
let want = dir.path().canonicalize().unwrap();
assert_eq!(
policy.fs.allow_write[0].as_path(),
want.as_path(),
"seeded allow_write path must be the canonicalized project root"
);
}
}
#[test]
fn policy_for_agent_allow_write_survives_nonexistent_root() {
let path = std::path::Path::new("/nonexistent/project/root/xyz");
for mode in [
crate::trust::TrustMode::Plan,
crate::trust::TrustMode::Safe,
crate::trust::TrustMode::Auto,
] {
let policy = policy_for_agent(mode, path);
assert_eq!(
policy.fs.allow_write.len(),
1,
"even with a missing root, allow_write must have one entry"
);
}
}
#[test]
fn policy_for_agent_seeds_git_deny_when_allow_git_config_is_false() {
let dir = tempfile::tempdir().unwrap();
let canonical = dir.path().canonicalize().unwrap();
for mode in [
crate::trust::TrustMode::Plan,
crate::trust::TrustMode::Safe,
crate::trust::TrustMode::Auto,
] {
let policy = policy_for_agent(mode, dir.path());
assert!(
!policy.fs.allow_git_config,
"strict_default precondition: allow_git_config must be false"
);
let denies: Vec<&std::path::Path> = policy
.fs
.deny_write_within_allow
.iter()
.map(|p| p.as_path())
.collect();
assert!(
denies.contains(&canonical.join(".git/hooks").as_path()),
"{mode:?}: deny_write_within_allow must contain .git/hooks, got {denies:?}"
);
assert!(
denies.contains(&canonical.join(".git/config").as_path()),
"{mode:?}: deny_write_within_allow must contain .git/config, got {denies:?}"
);
}
}
#[test]
fn compose_child_policy_inherits_parent_denies() {
let dir = tempfile::tempdir().unwrap();
let mut parent = SandboxPolicy::strict_default();
parent
.fs
.deny_read
.push(koda_sandbox::PathPattern::new("/etc/secrets"));
let composed = compose_child_policy(&parent, crate::trust::TrustMode::Safe, dir.path());
assert!(
composed
.fs
.deny_read
.contains(&koda_sandbox::PathPattern::new("/etc/secrets")),
"parent's deny_read must survive composing with the child"
);
}
#[test]
fn compose_child_policy_takes_tighter_wall_time() {
let dir = tempfile::tempdir().unwrap();
let mut parent = SandboxPolicy::strict_default();
parent.limits.wall_time_secs = Some(10);
let composed = compose_child_policy(&parent, crate::trust::TrustMode::Safe, dir.path());
assert_eq!(
composed.limits.wall_time_secs,
Some(10),
"parent's tighter wall_time must beat the child's looser default"
);
}
#[test]
fn compose_child_policy_takes_strictest_trust() {
let dir = tempfile::tempdir().unwrap();
let mut parent = SandboxPolicy::strict_default();
parent.trust = koda_sandbox::TrustPreference::Forbid;
let composed = compose_child_policy(&parent, crate::trust::TrustMode::Auto, dir.path());
assert_eq!(
composed.trust,
koda_sandbox::TrustPreference::Forbid,
"strictest trust wins regardless of which side it came from"
);
}
}