openmoat 0.4.0

Policy guard for AI coding agents: decides allow, ask or deny for every tool call from Claude Code, Codex and Cursor, configures Claude Code's and Codex's own OS sandboxes from the same policy, and keeps a local audit log.
use openmoat_core::DEFAULT_POLICY;
use serde_json::{Value, json};

use super::super::settings::{apply, in_sync, remove};
use super::super::tests::generated;
use super::super::{BLOCK_READS, Generated};
use super::apply_rules;

/// The default policy's settings with `rules` as the `permissions.deny` rules.
fn with_rules(rules: &[&str]) -> Generated {
    let mut out = generated(DEFAULT_POLICY);
    out.deny_rules = rules.iter().map(|r| (*r).to_owned()).collect();
    out
}

/// `moat uninstall` takes exactly the deny rules OpenMoat added: a user's rule
/// spelled like one of them stays, and the file comes back byte for byte.
#[test]
fn uninstall_removes_exactly_the_deny_rules_it_added() {
    let out = with_rules(&["Read(//Users/me/.ssh)", "Edit(//Users/me/.moat)"]);
    let original = json!({
        "permissions": { "deny": ["Read(//Users/me/.ssh)", "Edit(//tmp/x)", "Bash(rm *)"] },
        "theme": "dark",
    });
    let text = |v: &Value| serde_json::to_string_pretty(v).unwrap();
    let mut root = original.clone();
    let mut owned = Vec::new();
    assert!(apply(&mut root, &out, &mut owned).unwrap());
    assert_eq!(
        owned,
        ["Edit(//Users/me/.moat)"],
        "the user's rule is not ours"
    );
    assert!(in_sync(&root, &out, &owned));
    assert!(!apply(&mut root, &out, &mut owned).unwrap(), "idempotent");
    assert!(remove(&mut root, &owned));
    assert_eq!(text(&root), text(&original));
}

/// A rule OpenMoat added that the policy no longer produces is out of sync,
/// and the next apply takes it.
#[test]
fn a_rule_the_policy_dropped_goes_on_the_next_apply() {
    let out = with_rules(&["Edit(//Users/me/.moat)"]);
    let mut root = json!({ "permissions": { "deny": ["Read(//Users/me/.old)", "Bash(rm *)"] } });
    let mut owned = vec!["Read(//Users/me/.old)".to_owned()];
    assert!(!in_sync(&root, &out, &owned));
    apply(&mut root, &out, &mut owned).unwrap();
    assert!(in_sync(&root, &out, &owned));
    assert_eq!(
        root["permissions"]["deny"],
        json!(["Bash(rm *)", "Edit(//Users/me/.moat)"])
    );
}

/// Native Windows has no Claude Code sandbox, but its deny rules still apply.
#[test]
fn apply_rules_writes_only_the_deny_rules() {
    let out = generated(DEFAULT_POLICY);
    let original = json!({ "theme": "dark", "permissions": { "allow": ["Bash(ls)"] } });
    let mut root = original.clone();
    let mut owned = Vec::new();
    apply(&mut root, &out, &mut owned).unwrap();
    assert!(apply_rules(&mut root, &out, &mut owned).unwrap());
    assert!(
        root.get("sandbox").is_none(),
        "an older version's sandbox keys go"
    );
    assert!(root["permissions"].get(BLOCK_READS).is_none());
    assert_eq!(owned, out.deny_rules);
    assert!(
        !apply_rules(&mut root, &out, &mut owned).unwrap(),
        "idempotent"
    );
    assert!(remove(&mut root, &owned));
    assert_eq!(root, original);
}