openmoat 0.13.0

Policy checks for AI coding agents: decides allow, defer, ask or deny for every tool call from Claude Code, Codex, Cursor, OpenClaw, GitHub Copilot CLI, pi and Gemini CLI, configures Claude Code's, Codex's and Cursor's own OS sandboxes from the same policy, and keeps a local audit log.
//! `moat allowed`: what a person allowed, in one place, each with the command
//! that takes it back. Read-only, so it needs no terminal: tools that show it
//! (the `MoatBar` app) run it with `--format json`.
//!
//! - permanent rules `moat allow --always`, `--site` and `--dir` wrote to
//!   `~/.moat/policy.d/approved.yaml`;
//! - session grants from `moat allow --last` that have not expired;
//! - repositories whose policy `moat trust` lets allow.

use std::io::Write as _;

use anyhow::Result;
use openmoat_core::{Action, Kind, RuleGroup};
use serde::Serialize;

use crate::approvals::{self, GRANT_TTL_MS, Grants, Overlay};
use crate::cli::{AllowedArgs, Format};
use crate::exit::Code;
use crate::home::Home;
use crate::render::{self, Deferred};
use crate::time;
use crate::trust::Trust;

#[derive(Serialize)]
struct Allowed<'a> {
    /// Rules in `approved.yaml`, as the policy spells them.
    rules: &'a [RuleGroup],
    session_grants: Vec<SessionGrant<'a>>,
    trusted_repos: Vec<TrustedRepo<'a>>,
}

#[derive(Serialize)]
struct SessionGrant<'a> {
    host: &'a str,
    session_id: &'a str,
    /// One line a person can read: `run "npm install"`, `write ~/p/Makefile`.
    what: String,
    action: &'a Action,
    granted_at_ms: i64,
    expires_at_ms: i64,
    /// The project it applies in; `None` for a file grant, which needs none,
    /// and for one an earlier release wrote.
    project: Option<&'a str>,
    /// `false` for a grant an earlier release wrote without the project it
    /// needs: it applies nowhere until approved again.
    applies: bool,
}

#[derive(Serialize)]
struct TrustedRepo<'a> {
    root: &'a str,
    /// SHA-256 of the `.moat/policy.yaml` that was trusted; a change ends the trust.
    sha256: &'a str,
}

pub fn run(args: &AllowedArgs) -> Result<Code> {
    let home = Home::locate()?;
    let overlay = Overlay::load(&home.overlay_path())?;
    let grants = Grants::load(&home.grants_path())?;
    let trust = Trust::load(&home.trust_path())?;
    let now = time::now_ms();
    let allowed = Allowed {
        rules: &overlay.allow,
        session_grants: grants
            .active(now)
            .map(|g| SessionGrant {
                host: &g.host,
                session_id: &g.session_id,
                what: approvals::describe(&g.action),
                action: &g.action,
                granted_at_ms: g.granted_at_ms,
                expires_at_ms: g.granted_at_ms.saturating_add(GRANT_TTL_MS),
                project: g.project.as_deref(),
                applies: !g.unbound(),
            })
            .collect(),
        trusted_repos: trust
            .repos
            .iter()
            .map(|(root, sha256)| TrustedRepo { root, sha256 })
            .collect(),
    };
    if args.format == Format::Json {
        render::json(&allowed)?;
        return Ok(Code::Ok);
    }
    text(&allowed, now)
}

fn text(allowed: &Allowed<'_>, now: i64) -> Result<Code> {
    let mut out = Deferred::default();
    writeln!(out, "Permanent rules (~/.moat/policy.d/approved.yaml)")?;
    if allowed.rules.is_empty() {
        writeln!(out, "  none")?;
    }
    for rule in allowed.rules {
        writeln!(out, "  {:<14} {}", rule.id, patterns(rule))?;
        writeln!(
            out,
            "  {:<14} remove: openmoat allow --remove {}",
            "", rule.id
        )?;
    }
    writeln!(
        out,
        "Session grants (each ends {} after it was given)",
        time::duration(GRANT_TTL_MS)
    )?;
    if allowed.session_grants.is_empty() {
        writeln!(out, "  none")?;
    }
    for grant in &allowed.session_grants {
        let place = match (grant.applies, grant.project) {
            (false, _) => " (names no project, so it applies nowhere: approve it again)".to_owned(),
            (true, Some(project)) if matches!(grant.action, Action::Shell { .. }) => {
                format!(" in {project}")
            }
            (true, _) => String::new(),
        };
        render::line(
            &mut out,
            &format!(
                "  {} session {}: {}{place} (ends in {})",
                grant.host,
                grant.session_id,
                grant.what,
                time::duration(grant.expires_at_ms.saturating_sub(now))
            ),
        )?;
    }
    writeln!(out, "Trusted repositories (openmoat trust)")?;
    if allowed.trusted_repos.is_empty() {
        writeln!(out, "  none")?;
    }
    for repo in &allowed.trusted_repos {
        writeln!(out, "  {}  sha256:{}", repo.root, repo.sha256)?;
        writeln!(out, "  revoke: openmoat trust --revoke {}", repo.root)?;
    }
    out.finish()?;
    Ok(Code::Ok)
}

/// `shell: npm install; net: docs.rs`, as `moat trust` prints a rule.
fn patterns(group: &RuleGroup) -> String {
    Kind::ALL
        .into_iter()
        .filter(|k| !group.patterns(*k).is_empty())
        .map(|k| format!("{}: {}", k.as_str(), group.patterns(k).join(", ")))
        .collect::<Vec<_>>()
        .join("; ")
}