openmoat 0.1.0-alpha.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.
//! Command-line grammar.

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};
use openmoat_hosts::Host;

#[derive(Debug, Parser)]
#[command(
    name = "moat",
    version = concat!(env!("CARGO_PKG_VERSION"), " (OpenMoat)"),
    about = "OpenMoat: decides what AI coding agents may do, and records every decision",
    long_about = "OpenMoat decides what AI coding agents may do on this machine and records\n\
                  every decision.\n\
                  Alpha: the hook's decisions are not enforced by the operating system. The host\n\
                  sandboxes `moat init` configures, and `moat run`, confine commands at the OS\n\
                  level; outside them an allowed command runs with your permissions.\n\
                  Exit codes: 0 allow/ok, 1 the agent `run` started failed, 2 deny, 3 ask (unresolved),\n\
                  64 usage or configuration error\n\
                  (`guard` exits 2 instead, so a broken hook blocks rather than fails open)."
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// Install the kernel: default policy, audit log and host hooks.
    Init(InitArgs),
    /// Decide one hook request from stdin (invoked by host hooks, not by people).
    Guard(GuardArgs),
    /// Show one audit event, a session, or the most recent events.
    Show(ShowArgs),
    /// Report installation health: policy, hooks, recent activity.
    Status,
    /// Verify policy lock, hooks and binary; `--accept` re-pins edits made by a person.
    Doctor(DoctorArgs),
    /// Approve a shell command for one session, or permanently.
    Allow(AllowArgs),
    /// Let a repository's .moat/policy.yaml allow, not only deny and ask, until the file changes.
    /// Refused unless run from an interactive terminal, and over a drifted lock.
    Trust(TrustArgs),
    /// Replay agent sessions as a timeline of decisions.
    Replay(ReplayArgs),
    /// Summarise decisions over a window: verdicts, hosts, top rules, asks per hour.
    Report(ReportArgs),
    /// Run the default-deny egress proxy: only hosts the policy allows, every connection recorded.
    Proxy(ProxyArgs),
    /// Export the audit log for review elsewhere, and verify an export.
    Audit {
        #[command(subcommand)]
        command: AuditCommand,
    },
    /// Run an agent in a sandbox generated from the policy, its network only through
    /// OpenMoat's proxy (Lightweight tier, ADR-018; macOS, Linux).
    /// The agent's own sandbox must be off.
    Run(RunArgs),
    /// Inspect and test policy files.
    Policy {
        #[command(subcommand)]
        command: PolicyCommand,
    },
    /// The host sandboxes OpenMoat configures from the policy (Standard tier, ADR-018).
    Sandbox {
        #[command(subcommand)]
        command: SandboxCommand,
    },
}

#[derive(Debug, Subcommand)]
pub enum SandboxCommand {
    /// Print the sandbox settings the policy compiles to and the translation losses; writes nothing.
    Show(SandboxShowArgs),
    /// Write the sandbox settings from the current policy and re-pin them.
    /// Refused unless run from an interactive terminal, and over a drifted lock.
    Sync,
}

#[derive(Debug, Args)]
pub struct SandboxShowArgs {
    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Args)]
pub struct InitArgs {
    /// Hosts to install hooks for. Defaults to every supported host that is present.
    #[arg(long, value_delimiter = ',', value_parser = parse_host)]
    pub hosts: Option<Vec<Host>>,

    /// Print what would change without writing anything.
    #[arg(long)]
    pub dry_run: bool,
}

#[derive(Debug, Args)]
pub struct ReplayArgs {
    /// Window start: all, today, yesterday, 12h, 7d, 2w or YYYY-MM-DD (UTC).
    #[arg(long, default_value = "24h", conflicts_with = "session")]
    pub since: String,

    /// Only this host.
    #[arg(long, value_parser = parse_host, conflicts_with = "session")]
    pub host: Option<Host>,

    /// One session by id, regardless of window.
    #[arg(long)]
    pub session: Option<String>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Args)]
pub struct ReportArgs {
    /// Window start: all, today, yesterday, 12h, 7d, 2w or YYYY-MM-DD (UTC).
    #[arg(long, default_value = "7d")]
    pub since: String,

    /// Only this host.
    #[arg(long, value_parser = parse_host)]
    pub host: Option<Host>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Args)]
pub struct AllowArgs {
    /// The exact shell command to approve.
    #[arg(conflicts_with = "last")]
    pub command: Option<String>,

    /// Take host, session and command from the most recent `ask` in the audit log.
    #[arg(long)]
    pub last: bool,

    /// Host of the session to approve (with --session).
    #[arg(long, value_parser = parse_host, requires = "session")]
    pub host: Option<Host>,

    /// Approve for this session id only (exact command match).
    #[arg(long, conflicts_with = "always")]
    pub session: Option<String>,

    /// Add a permanent allow rule to ~/.moat/policy.d/approved.yaml instead.
    #[arg(long)]
    pub always: bool,
}

#[derive(Debug, Args)]
pub struct TrustArgs {
    /// A directory in the repository. Defaults to the current directory.
    pub repo: Option<PathBuf>,

    /// Withdraw the trust instead: the repository policy only tightens again.
    #[arg(long)]
    pub revoke: bool,
}

#[derive(Debug, Args)]
pub struct DoctorArgs {
    /// Accept the current policy and hook files as trusted and re-pin the lock.
    /// Refused unless run from an interactive terminal.
    #[arg(long)]
    pub accept: bool,
}

#[derive(Debug, Args)]
pub struct ProxyArgs {
    /// Address to listen on. Must be a loopback address: the proxy serves this machine only.
    /// Default: 127.0.0.1 and the policy's `sandbox.proxy_port`, where the host sandboxes
    /// then send their commands' traffic; 127.0.0.1:18080 when it is not set.
    #[arg(long)]
    pub listen: Option<std::net::SocketAddr>,
}

#[derive(Debug, Args)]
pub struct RunArgs {
    /// A file or directory the agent and its commands may also read and write, such as
    /// the agent's own state (`~/.claude`); deny rules still win. Repeatable.
    #[arg(long = "write", value_name = "PATH")]
    pub writes: Vec<PathBuf>,

    /// The agent and its arguments, after `--`.
    #[arg(required = true, last = true, value_name = "AGENT")]
    pub command: Vec<std::ffi::OsString>,
}

#[derive(Debug, Args)]
pub struct GuardArgs {
    /// Host whose hook payload is on stdin.
    #[arg(long, value_parser = parse_host)]
    pub host: Host,
}

#[derive(Debug, Args)]
pub struct ShowArgs {
    /// Event id as printed by `guard` (hex).
    #[arg(conflicts_with = "session")]
    pub id: Option<String>,

    /// All events of one host session, oldest first.
    #[arg(long)]
    pub session: Option<String>,

    /// Events since a window start, oldest first: all, today, yesterday, 12h, 7d, 2w or YYYY-MM-DD (UTC).
    #[arg(long, conflicts_with_all = ["id", "session"])]
    pub since: Option<String>,

    /// Most recent events (default when nothing else is given).
    #[arg(long, default_value_t = 10)]
    pub recent: usize,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Subcommand)]
pub enum AuditCommand {
    /// Write events to stdout as JSON Lines, oldest first, each with its chain hashes.
    Export(ExportArgs),
    /// Check an export's hash chain without the database and print its head hash.
    Verify(VerifyArgs),
    /// Report over exports from several machines: per host, per rule, top asks and
    /// denies, false-positive candidates. Every file must verify first.
    Report(TeamReportArgs),
}

#[derive(Debug, Args)]
pub struct TeamReportArgs {
    /// Files `moat audit export` wrote; events present in several are counted once.
    #[arg(required = true)]
    pub files: Vec<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Args)]
pub struct VerifyArgs {
    /// The file `moat audit export` wrote.
    pub file: PathBuf,

    /// A head hash recorded earlier (`moat audit verify` or `moat doctor` printed it);
    /// fail unless a verified event carries it.
    #[arg(long)]
    pub anchor: Option<String>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Args)]
pub struct ExportArgs {
    /// Window start: all, today, yesterday, 12h, 7d, 2w or YYYY-MM-DD (UTC).
    #[arg(long, default_value = "all")]
    pub since: String,

    /// Only this host.
    #[arg(long, value_parser = parse_host)]
    pub host: Option<Host>,

    /// Only this session id.
    #[arg(long)]
    pub session: Option<String>,
}

#[derive(Debug, Subcommand)]
pub enum PolicyCommand {
    /// Validate a policy file and print a summary.
    Lint(LintArgs),
    /// Decide one action against a policy and explain the result.
    Check(CheckArgs),
    /// Print what OS layers enforce for a policy: the compiled IR and its losses.
    Compile(CompileArgs),
}

#[derive(Debug, Args)]
pub struct LintArgs {
    /// Policy file to validate. Defaults to the installed user policy.
    pub file: Option<PathBuf>,
}

#[derive(Debug, Args)]
pub struct CheckArgs {
    /// The action: a shell command, a path, a URL or an MCP tool name, depending on --kind.
    pub action: String,

    /// Policy file to evaluate against. Defaults to the installed user policy.
    #[arg(long, short)]
    pub policy: Option<PathBuf>,

    /// Kind of action.
    #[arg(long, value_enum, default_value_t = ActionKind::Shell)]
    pub kind: ActionKind,

    /// Trusted project root that `${project}` expands to. Defaults to the git root of --cwd.
    #[arg(long)]
    pub project: Option<PathBuf>,

    /// Working directory the action runs in. Defaults to the current directory.
    #[arg(long)]
    pub cwd: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Args)]
pub struct CompileArgs {
    /// Policy file to compile. Defaults to the installed user policy.
    #[arg(long, short)]
    pub policy: Option<PathBuf>,

    /// Trusted project root that `${project}` expands to. Defaults to the git root of --cwd.
    #[arg(long)]
    pub project: Option<PathBuf>,

    /// Working directory to compile for. Defaults to the current directory.
    #[arg(long)]
    pub cwd: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    pub format: Format,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ActionKind {
    Shell,
    FsRead,
    FsWrite,
    Net,
    /// A URL read by a host fetch tool (`WebFetch`).
    Fetch,
    Mcp,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
    Text,
    Json,
}

fn parse_host(value: &str) -> Result<Host, String> {
    value
        .parse()
        .map_err(|e: openmoat_hosts::HostError| e.to_string())
}