use openmoat_core::{DEFAULT_POLICY, Policy};
use serde_json::{Value, json};
use super::*;
use crate::sandbox::{assert_golden, lower_for_hosts};
pub(super) fn generated(yaml: &str) -> Generated {
let policy = Policy::parse(yaml).expect("test policy lints");
let ir = lower_for_hosts(&policy, "/Users/me", None, Vec::new(), false).expect("lowers");
generate(&ir, crate::sandbox::proxy_port(&policy)).expect("generates")
}
fn list(generated: &Generated, pointer: &str) -> Vec<String> {
let value = Value::Object(generated.sandbox.clone());
serde_json::from_value(value.pointer(pointer).cloned().unwrap_or(json!([]))).unwrap()
}
#[test]
fn default_policy_matches_the_golden_settings() {
let out = generated(DEFAULT_POLICY);
let actual = serde_json::to_string_pretty(&json!({
"sandbox": out.sandbox,
"permissions": { BLOCK_READS: out.block_reads },
"losses": out.report.losses.iter().map(ToString::to_string).collect::<Vec<_>>(),
"allowances": out.report.allowances.iter().map(ToString::to_string).collect::<Vec<_>>(),
}))
.unwrap();
assert_golden("claude-default.json", &(actual + "\n"));
}
#[test]
fn every_pattern_is_absolute() {
let out = generated(DEFAULT_POLICY);
for pointer in [
"/filesystem/denyRead",
"/filesystem/allowRead",
"/filesystem/denyWrite",
] {
for p in list(&out, pointer) {
assert!(
p.starts_with('/'),
"{pointer}: {p} would resolve against ~/.claude"
);
}
}
let deny = list(&out, "/filesystem/denyRead");
for expected in [
"/**/.env",
"/**/.env.*",
"/Users/me/.ssh",
"/Users/me/.cargo/credentials.toml",
] {
assert!(deny.contains(&expected.to_owned()), "{expected}: {deny:?}");
}
assert!(list(&out, "/filesystem/allowRead").contains(&"/Users/me/.cargo".to_owned()));
assert!(
!list(&out, "/filesystem/allowRead")
.iter()
.any(|p| p.contains(".env")),
"a `**/` exception would re-open .env.example everywhere"
);
assert!(out.block_reads);
}
#[test]
fn directory_nodes_with_entries_below_are_left_out_and_listed() {
let out = generated(DEFAULT_POLICY);
let deny = list(&out, "/filesystem/denyWrite");
assert!(
!deny.contains(&"/**/.claude".to_owned()),
"would make .claude/worktrees read-only"
);
assert!(deny.contains(&"/**/.claude/settings.json".to_owned()));
assert!(
deny.contains(&"/Users/me/.codex".to_owned()),
"a tree rule keeps its directory"
);
let nodes = out
.report
.allowances
.iter()
.find(|a| a.rule == "claude-code.directory-nodes");
assert!(nodes.is_some_and(|a| a.patterns.contains(&"/**/.claude".to_owned())));
}
#[test]
fn network_lists_domains_and_keeps_addresses_denied() {
let out = generated(DEFAULT_POLICY);
let allowed = list(&out, "/network/allowedDomains");
assert!(allowed.contains(&"registry.npmjs.org".to_owned()));
assert!(
!allowed.iter().any(|h| h == "localhost"),
"an ask is a deny"
);
let denied = list(&out, "/network/deniedDomains");
assert!(denied.contains(&"metadata.google.internal".to_owned()));
assert!(!denied.iter().any(|h| h.contains("169.254")));
assert_eq!(out.sandbox["network"]["strictAllowlist"], json!(true));
}
#[test]
fn both_proxy_ports_name_moat_proxy_only_when_the_policy_opts_in() {
let out = generated(DEFAULT_POLICY);
assert!(out.sandbox["network"].get("httpProxyPort").is_none());
assert!(out.sandbox["network"].get("socksProxyPort").is_none());
assert!(
!out.report
.losses
.iter()
.any(|l| l.rule == "claude-code.proxy")
);
let own = generated("version: 1\nsandbox:\n proxy_port: 18555\n");
assert_eq!(own.sandbox["network"]["httpProxyPort"], json!(18555));
assert_eq!(own.sandbox["network"]["socksProxyPort"], json!(18555));
assert!(
own.report
.losses
.iter()
.any(|l| l.rule == "claude-code.proxy" && l.message.contains("127.0.0.1:18555")),
"{:?}",
own.report.losses
);
}
#[test]
fn a_read_root_inside_a_deny_is_not_reopened() {
let out = generated(
"version: 1\ndeny:\n - id: s\n fs.read: ['~/.ssh/**']\nsandbox:\n read_roots: ['~/.ssh/pub', '/usr']\n",
);
assert_eq!(list(&out, "/filesystem/allowRead"), ["/usr"]);
assert!(
out.report
.losses
.iter()
.any(|l| l.message.contains("/Users/me/.ssh/pub"))
);
}
#[test]
fn git_is_writable_except_what_makes_it_run_code() {
let out = generated(DEFAULT_POLICY);
let deny = list(&out, "/filesystem/denyWrite");
for whole in ["/**/.git", "/**/.git/**"] {
assert!(
!deny.contains(&whole.to_owned()),
"git commit needs {whole}"
);
}
for vector in [
"/**/.git/hooks",
"/**/.git/hooks/**",
"/**/.git/config",
"/**/.git/config.worktree",
"/**/.git/info/attributes",
"/**/.git/worktrees/*/config.worktree",
"/**/.git/worktrees/*/commondir",
"/**/.git/modules/**/hooks",
"/**/.git/modules/**/config",
] {
assert!(deny.contains(&vector.to_owned()), "{vector}: {deny:?}");
}
let allowance = out
.report
.allowances
.iter()
.find(|a| a.rule == "claude-code.git-internals");
assert!(allowance.is_some_and(|a| a.patterns == ["/**/.git/**"]));
assert!(
!out.report.losses.iter().any(|l| l.message.contains(".git")),
"{:?}",
out.report.losses
);
assert!(
deny.contains(&"/**/.moat".to_owned()),
"other exceptions stay denied"
);
}
#[test]
fn a_policy_that_denies_git_keeps_it_denied() {
let out = generated(
"version: 1\ndeny:\n - id: g\n fs.write: ['**/.git/**']\nallow:\n - id: p\n fs.write: ['${project}/**', '!${project}/.git/**']\n",
);
let deny = list(&out, "/filesystem/denyWrite");
assert!(deny.contains(&"/**/.git".to_owned()), "{deny:?}");
}