openmoat 0.6.0

Policy checks for AI coding agents: decides allow, defer, 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 std::collections::HashMap;
use std::io;

use super::{Process, agent_name, walk};

fn argv(line: &str) -> Vec<String> {
    line.split(' ').map(str::to_owned).collect()
}

#[test]
fn agent_hosts_are_known_by_program_or_script() {
    for (line, agent) in [
        ("claude", "claude"),
        ("/Users/me/.local/bin/claude --resume", "claude"),
        ("/Users/me/.local/share/claude/versions/2.1.283", "claude"),
        (
            "node /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js",
            "claude-code",
        ),
        ("node /usr/local/bin/codex exec", "codex"),
        (
            "/opt/codex/vendor/aarch64-apple-darwin/codex/codex",
            "codex",
        ),
        ("C:\\Tools\\Codex.exe", "codex"),
        (
            "node /home/me/.local/share/cursor-agent/versions/1.0/index.js",
            "cursor-agent",
        ),
        ("cursor-agent", "cursor-agent"),
        ("/usr/local/bin/moat run -- claude", "moat"),
    ] {
        assert_eq!(agent_name(&argv(line)).as_deref(), Some(agent), "{line}");
    }
}

#[test]
fn a_persons_shell_and_tools_are_not_agents() {
    for line in [
        "-zsh",
        "/bin/bash --login",
        "/usr/bin/login -pfl me /bin/zsh",
        "/Applications/Cursor.app/Contents/MacOS/Cursor",
        "tmux new -s claude",
        "/Users/me/moat/bin/build.sh",
        "/Users/me/src/openmoat/target/debug/deps/e2e-1234",
        "vim claude.md",
        "sshd: me@pts/0",
    ] {
        assert_eq!(agent_name(&argv(line)), None, "{line}");
    }
}

fn table(rows: &[(u32, u32, &str)]) -> HashMap<u32, Process> {
    rows.iter()
        .map(|(pid, ppid, line)| {
            (
                *pid,
                Process {
                    ppid: *ppid,
                    argv: argv(line),
                },
            )
        })
        .collect()
}

fn walk_in(table: &HashMap<u32, Process>, from: u32) -> io::Result<Option<String>> {
    walk(from, |pid| {
        table
            .get(&pid)
            .cloned()
            .ok_or_else(|| io::Error::other("gone"))
    })
}

#[test]
fn an_agent_anywhere_above_is_found_through_wrappers() {
    let t = table(&[
        (1, 0, "/sbin/launchd"),
        (50, 1, "/Applications/iTerm.app/Contents/MacOS/iTerm2"),
        (60, 50, "-zsh"),
        (70, 60, "claude"),
        (80, 70, "/bin/zsh -c script -q /dev/null moat allow"),
        (90, 80, "script -q /dev/null moat allow"),
    ]);
    assert_eq!(walk_in(&t, 90).unwrap().as_deref(), Some("claude"));
    assert_eq!(walk_in(&t, 60).unwrap(), None, "a person's own shell");
}

#[test]
fn an_agent_as_the_container_init_is_found() {
    let t = table(&[
        (1, 0, "claude --dangerously-skip-permissions"),
        (5, 1, "sh"),
    ]);
    assert_eq!(walk_in(&t, 5).unwrap().as_deref(), Some("claude"));
}

#[test]
fn a_missing_parent_fails_but_a_hidden_init_ends_the_walk() {
    let t = table(&[(5, 4, "sh")]);
    assert!(walk_in(&t, 5).is_err(), "pid 4 cannot be read");
    let t = table(&[(5, 1, "sh")]);
    assert_eq!(walk_in(&t, 5).unwrap(), None, "pid 1 hidden");
}

#[test]
fn a_loop_in_the_table_ends() {
    let t = table(&[(5, 6, "sh"), (6, 5, "sh")]);
    assert!(walk_in(&t, 5).is_err());
    let t = table(&[(5, 5, "sh")]);
    assert_eq!(walk_in(&t, 5).unwrap(), None);
}

#[cfg(target_os = "linux")]
#[test]
fn proc_stat_and_cmdline_are_read() {
    let p =
        super::linux::parse("42 (my (odd) name) S 7 42 42 0", b"node\0/usr/bin/codex\0").unwrap();
    assert_eq!(p.ppid, 7);
    assert_eq!(p.argv, ["node", "/usr/bin/codex"]);
    let zombie = super::linux::parse("42 (claude) Z 7 42", b"").unwrap();
    assert_eq!(zombie.argv, ["claude"]);
    assert!(super::linux::parse("garbage", b"").is_err());
}

#[cfg(target_os = "macos")]
#[test]
fn ps_lines_are_read() {
    let t = super::macos::parse("    1     0 /sbin/launchd\n  420   1 /bin/zsh -l\nbad line\n");
    assert_eq!(t[&420].ppid, 1);
    assert_eq!(t[&420].argv, ["/bin/zsh", "-l"]);
    assert_eq!(t.len(), 2);
}