openmoat 0.1.0-alpha.7

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 healthy = true;
    let mut out = io::stdout().lock();

    writeln!(out, "state directory  {}", home.root().display())?;

    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"
            )?;
        }
    }

    let lock_path = home.lock_path();
    match Lock::load(&lock_path) {
        Ok(lock) => {
            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()
                )?;
            }
        }
        Err(_) if !lock_path.exists() => {
            healthy = false;
            writeln!(out, "lock             ✗ missing (run `moat init`)")?;
        }
        Err(error) => {
            healthy = false;
            writeln!(out, "lock             ✗ {error:#}")?;
        }
    }

    match Grants::load(&home.grants_path()) {
        Ok(grants) => {
            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")?,
            }
        }
        Err(error) => {
            healthy = false;
            writeln!(out, "approvals        ✗ {error:#}")?;
        }
    }

    let recorded = Recorded::load(&home).unwrap_or_default();
    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()
        )?;
    }

    if let Ok(policy) = home.load_policy() {
        healthy &= sandboxes(&mut out, &Plan::new(&policy)?, &lock_path, &recorded)?;
    }
    // Derived from the hook and sandbox lines above, so it leaves `healthy` alone.
    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)?;
        }
    }

    let audit_path = home.audit_path();
    if audit_path.is_file() {
        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)?;
        }
    } else {
        healthy = false;
        writeln!(out, "audit log        ✗ missing (run `moat init`)")?;
    }

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

/// 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 { "!" }
        )?;
    }
    Ok(healthy)
}