openmoat 0.2.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.
//! `moat status`: is the kernel installed, current and active?

use std::fs;
use std::io::{self, Write as _};
use std::path::Path;

use anyhow::Result;
use openmoat_audit::Store;
use openmoat_hosts::Host;

use crate::approvals::{GRANT_TTL_MS, Grants};
use crate::cli::{Format, StatusArgs};
use crate::exit::Code;
use crate::home::Home;
use crate::install::{HookState, HostConfig, Recorded};
use crate::integrity::{self, HookPinGap, Lock};
use crate::sandbox::{Plan, install as host_sandbox};
use crate::{protection, render, time};

pub fn run(args: &StatusArgs) -> Result<Code> {
    let home = Home::locate()?;
    let binary = crate::install::hook_binary()?;
    if args.format == Format::Json {
        render::json(&serde_json::json!({ "agents": protection::report(&home, &binary)? }))?;
        return Ok(Code::Ok);
    }
    let mut out = io::stdout().lock();

    writeln!(out, "state directory  {}", home.root().display())?;
    let mut healthy = policies(&mut out, &home)?;
    healthy &= lock(&mut out, &home, &binary)?;
    healthy &= approvals(&mut out, &home)?;
    let recorded = Recorded::load(&home).unwrap_or_default();
    healthy &= hooks(&mut out, &binary, &recorded)?;
    if let Ok(policy) = home.load_policy() {
        healthy &= sandboxes(&mut out, &Plan::new(&policy)?, &home.lock_path(), &recorded)?;
    }
    // Derived from the hook and sandbox lines above, so it leaves `healthy` alone.
    protection_lines(&mut out, &home, &binary)?;
    healthy &= audit(&mut out, &home)?;

    Ok(if healthy { Code::Ok } else { Code::Usage })
}

/// The policy line and, in a project with one, the repository policy line;
/// `false` on any problem.
fn policies(out: &mut impl io::Write, home: &Home) -> Result<bool> {
    let mut healthy = true;
    let policy_path = home.policy_path();
    match home.load_policy() {
        Ok(policy) => {
            let bytes = fs::read(&policy_path)?;
            let digest = crate::integrity::sha256_hex(&bytes);
            let (deny, allow, ask) = policy.rule_count();
            writeln!(
                out,
                "policy           {}  sha256:{digest}  ({deny} deny, {allow} allow, {ask} ask)",
                policy_path.display()
            )?;
        }
        Err(error) => {
            healthy = false;
            writeln!(out, "policy           ✗ {error:#}")?;
        }
    }
    match crate::repo::summary(home) {
        Ok(Some(line)) => writeln!(out, "repo policy      {line}")?,
        Ok(None) => {}
        Err(error) => {
            healthy = false;
            writeln!(
                out,
                "repo policy      ✗ {error:#}; every call in this project is denied"
            )?;
        }
    }
    Ok(healthy)
}

/// The lock lines: drift, and hook files the lock does not pin; `false` on any
/// problem.
fn lock(out: &mut impl io::Write, home: &Home, binary: &Path) -> Result<bool> {
    let lock_path = home.lock_path();
    let lock = match Lock::load(&lock_path) {
        Ok(lock) => lock,
        Err(_) if !lock_path.exists() => {
            writeln!(out, "lock             ✗ missing (run `moat init`)")?;
            return Ok(false);
        }
        Err(error) => {
            writeln!(out, "lock             ✗ {error:#}")?;
            return Ok(false);
        }
    };
    let mut healthy = true;
    let drift = lock.verify();
    if drift.is_empty() {
        writeln!(
            out,
            "lock             ✔ {} files pinned, intact",
            lock.entries.len()
        )?;
    } else {
        healthy = false;
        for d in drift {
            writeln!(out, "lock             ✗ {d} (run `moat doctor`)")?;
        }
    }
    let installed = integrity::installed_hook_files(binary)?;
    let gap = HookPinGap::new(&lock, home, &installed);
    for path in gap.unpinned {
        healthy = false;
        writeln!(
            out,
            "lock             ✗ {} is not pinned (run `moat init` to pin it)",
            path.display()
        )?;
    }
    for path in gap.elsewhere {
        writeln!(
            out,
            "lock             · {} is pinned but not this shell's hook file (CLAUDE_CONFIG_DIR, CODEX_HOME or CURSOR_CONFIG_DIR differ)",
            path.display()
        )?;
    }
    Ok(healthy)
}

/// The session grants line; `false` when the grants file is unreadable.
fn approvals(out: &mut impl io::Write, home: &Home) -> Result<bool> {
    let grants = match Grants::load(&home.grants_path()) {
        Ok(grants) => grants,
        Err(error) => {
            writeln!(out, "approvals        ✗ {error:#}")?;
            return Ok(false);
        }
    };
    let now = time::now_ms();
    let ages: Vec<i64> = grants.active(now).map(|g| now - g.granted_at_ms).collect();
    match ages.iter().max() {
        Some(oldest) => writeln!(
            out,
            "approvals        {} active session grant(s), oldest {} old (each expires after {})",
            ages.len(),
            time::duration(*oldest),
            time::duration(GRANT_TTL_MS)
        )?,
        None => writeln!(out, "approvals        no active session grants")?,
    }
    Ok(true)
}

/// One line per host's hook, then the Continue CLI warning; `false` on any
/// problem.
fn hooks(out: &mut impl io::Write, binary: &Path, recorded: &Recorded) -> Result<bool> {
    let mut healthy = true;
    for host in Host::ALL {
        let config = HostConfig::for_host(host)?;
        let line = match config.state(binary) {
            HookState::Installed => "✔ installed".to_owned(),
            HookState::Missing if recorded.skipped(host) && config.host_present() => {
                format!("· not set up (`moat init --hosts {}` adds it)", host.id())
            }
            HookState::Missing if !config.host_present() => "· host not found".to_owned(),
            HookState::Missing => {
                healthy = false;
                "✗ hook missing (run `moat init`)".to_owned()
            }
            HookState::Stale { command } => {
                healthy = false;
                format!("✗ {}", crate::install::stale_hint(&command))
            }
            HookState::Unreadable(error) => {
                healthy = false;
                format!("✗ {error}")
            }
        };
        writeln!(
            out,
            "{:<16} {line}  {}",
            host.display_name(),
            config.settings_path.display()
        )?;
    }
    if let Some(found) = crate::install::continue_cli()? {
        writeln!(
            out,
            "{:<16} · {}  {}",
            Host::Continue.display_name(),
            crate::install::CONTINUE_CLI_WARNING,
            found.display()
        )?;
    }
    Ok(healthy)
}

/// Each agent's protection level and known gaps.
fn protection_lines(out: &mut impl io::Write, home: &Home, binary: &Path) -> Result<()> {
    for agent in protection::report(home, binary)? {
        if let Some(summary) = agent.summary() {
            let mark = match agent.level {
                protection::Level::HookAndOsSandbox => "✔",
                protection::Level::HookOnly => "!",
                _ => "✗",
            };
            writeln!(out, "{:<16} {mark} protection: {summary}", agent.name)?;
            writeln!(out, "{:<16} · known gaps: {}", agent.name, agent.gaps)?;
        }
    }
    Ok(())
}

/// The audit log line and the most recent events; `false` when the log is
/// missing.
fn audit(out: &mut impl io::Write, home: &Home) -> Result<bool> {
    let audit_path = home.audit_path();
    if !audit_path.is_file() {
        writeln!(out, "audit log        ✗ missing (run `moat init`)")?;
        return Ok(false);
    }
    let store = Store::open_read_only(&audit_path)?;
    writeln!(
        out,
        "audit log        {}  ({} events)",
        audit_path.display(),
        store.count()?
    )?;
    let recent = store.recent(5)?;
    if !recent.is_empty() {
        writeln!(out)?;
        render::event_table(&recent)?;
    }
    Ok(true)
}

/// One line per present host's sandbox (Standard tier); `false` on any problem.
fn sandboxes(
    out: &mut impl io::Write,
    plan: &Plan,
    lock_path: &Path,
    recorded: &Recorded,
) -> Result<bool> {
    let lock = Lock::load(lock_path).ok();
    let mut healthy = true;
    let mut present = false;
    for host in host_sandbox::HOSTS
        .into_iter()
        .filter(|h| !recorded.skipped(*h))
    {
        let name = host.display_name();
        let problems = host_sandbox::problems(host, plan, lock.as_ref());
        if let (Ok(Some(_)), Some(note)) = (&problems, host_sandbox::unavailable(host)) {
            writeln!(out, "{name:<16} · {note}")?;
            continue;
        }
        present |= !matches!(problems, Ok(None));
        match problems {
            Ok(None) => {}
            Ok(Some(problems)) if problems.is_empty() => {
                let report = host_sandbox::report(host, plan);
                writeln!(
                    out,
                    "{name:<16} ✔ sandbox matches the policy ({} stricter, {} wider: `moat sandbox show`)",
                    report.losses.len(),
                    report.allowances.len()
                )?;
            }
            Ok(Some(problems)) => {
                healthy = false;
                for problem in problems {
                    writeln!(out, "{name:<16} ✗ sandbox: {problem}")?;
                }
            }
            Err(error) => {
                healthy = false;
                writeln!(out, "{name:<16} ✗ sandbox: {error:#}")?;
            }
        }
    }
    if let Some(port) = plan.proxy_port.filter(|_| present) {
        // A warning that leaves `healthy` alone: a stopped proxy fails closed.
        let (ok, text) = super::proxy::listening_line(port);
        writeln!(
            out,
            "moat proxy       {} {text}",
            if ok { "✔" } else { "!" }
        )?;
    }
    let home = Home::locate()?;
    if let Some((ok, text)) = super::proxy::service_line(&home) {
        writeln!(
            out,
            "moat proxy       {} {text}",
            if ok { "✔" } else { "!" }
        )?;
        healthy &= ok;
    }
    Ok(healthy)
}