Skip to main content

agentstatedeveloper_cli/commands/
audit.rs

1//! `asd audit …` — read back the JSONL audit log.
2
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use clap::{Args, Subcommand};
7
8use agentstatedeveloper_core::{AuditEvent, read_jsonl};
9
10use crate::config::Config;
11
12#[derive(Debug, Subcommand)]
13pub enum AuditCmd {
14    /// Print audit events from the configured audit log. Supports
15    /// filtering by event type and by "since" event id (exclusive).
16    Tail(TailArgs),
17
18    /// Verify the hash-chain integrity of the audit log. Commercial
19    /// feature — requires `asd-pro` (Enterprise tier).
20    Verify(VerifyArgs),
21}
22
23#[derive(Debug, Args)]
24pub struct VerifyArgs {
25    /// Override the audit log path. Defaults to `--audit-log` /
26    /// `ASD_AUDIT_LOG` from config.
27    #[arg(long)]
28    pub log: Option<PathBuf>,
29}
30
31#[derive(Debug, Args)]
32pub struct TailArgs {
33    /// Override the audit log path. Defaults to `--audit-log` /
34    /// `ASD_AUDIT_LOG` from config.
35    #[arg(long)]
36    pub log: Option<PathBuf>,
37
38    /// Filter by event type substring (e.g., `ledger.approve`,
39    /// `ledger.` for all ledger events).
40    #[arg(long)]
41    pub event_type: Option<String>,
42
43    /// Return only events AFTER this `event_id` (exclusive). Useful
44    /// for incremental polling.
45    #[arg(long)]
46    pub since: Option<String>,
47
48    /// Filter by actor id.
49    #[arg(long)]
50    pub actor: Option<String>,
51
52    /// Filter by outcome (success, denied, awaiting-approval,
53    /// already-resolved, error, unauthorized).
54    #[arg(long)]
55    pub outcome: Option<String>,
56
57    /// Max events to return (default: 200).
58    #[arg(long, default_value_t = 200)]
59    pub limit: usize,
60}
61
62pub fn run(cfg: &Config, cmd: AuditCmd) -> Result<()> {
63    match cmd {
64        AuditCmd::Tail(args) => tail(cfg, args),
65        AuditCmd::Verify(args) => verify(cfg, args),
66    }
67}
68
69fn verify(_cfg: &Config, _args: VerifyArgs) -> Result<()> {
70    anyhow::bail!(
71        "audit verify is a commercial feature (Enterprise tier) — \
72         install asd-pro to enable tamper-evident chain verification. \
73         See https://agentstatedeveloper.dev/pricing"
74    )
75}
76
77fn tail(cfg: &Config, args: TailArgs) -> Result<()> {
78    let path = args
79        .log
80        .as_ref()
81        .or(cfg.audit_log_path.as_ref())
82        .ok_or_else(|| {
83            anyhow::anyhow!(
84                "no audit log configured — pass --log <path>, \
85                 set --audit-log <path> globally, or export ASD_AUDIT_LOG"
86            )
87        })?;
88
89    let events = read_jsonl(path).with_context(|| format!("read audit log {}", path.display()))?;
90
91    // Apply `since` cursor first (drop up to and including the matching id).
92    let start_idx = match args.since {
93        Some(ref id) => events
94            .iter()
95            .position(|e| &e.event_id == id)
96            .map(|i| i + 1)
97            .unwrap_or(0),
98        None => 0,
99    };
100
101    let filtered: Vec<&AuditEvent> = events[start_idx..]
102        .iter()
103        .filter(|e| {
104            if let Some(ref t) = args.event_type {
105                if !e.event_type.contains(t) {
106                    return false;
107                }
108            }
109            if let Some(ref a) = args.actor {
110                if &e.actor_id != a {
111                    return false;
112                }
113            }
114            if let Some(ref o) = args.outcome {
115                if &e.outcome != o {
116                    return false;
117                }
118            }
119            true
120        })
121        .take(args.limit)
122        .collect();
123
124    let out = serde_json::json!({
125        "path": path.display().to_string(),
126        "count": filtered.len(),
127        "events": filtered,
128    });
129    println!("{}", serde_json::to_string_pretty(&out)?);
130    Ok(())
131}