use anyhow::{Context, Result, anyhow};
use clap::{Args, Subcommand};
use serde_json::json;
use agentstatedeveloper_core::{Decision, FilePolicyGate, PolicyFile, PolicyGate, Situation};
use crate::config::Config;
#[derive(Debug, Subcommand)]
pub enum PolicyCmd {
List(ListArgs),
Show(ShowArgs),
Evaluate(EvaluateArgs),
}
#[derive(Debug, Args)]
pub struct ListArgs {
#[arg(long)]
pub prefix: Option<String>,
}
#[derive(Debug, Args)]
pub struct ShowArgs {
pub path: String,
}
#[derive(Debug, Args)]
pub struct EvaluateArgs {
pub action: String,
#[arg(long, default_value = "asd-cli-user")]
pub agent_id: String,
#[arg(long)]
pub description: Option<String>,
}
pub fn run(cfg: &Config, cmd: PolicyCmd) -> Result<()> {
let path = cfg.policy_path.as_ref().ok_or_else(|| {
anyhow!("no policy file configured — pass --policy <path> or set ASD_POLICY")
})?;
match cmd {
PolicyCmd::List(args) => list(path, args),
PolicyCmd::Show(args) => show(path, args),
PolicyCmd::Evaluate(args) => evaluate(path, args),
}
}
fn list(path: &std::path::Path, args: ListArgs) -> Result<()> {
let file =
PolicyFile::load(path).with_context(|| format!("load policy file {}", path.display()))?;
let filtered: Vec<_> = file
.policies
.iter()
.filter(|r| args.prefix.as_deref().is_none_or(|p| r.path.starts_with(p)))
.collect();
let out = json!({
"source": path.display().to_string(),
"strict": file.strict,
"count": filtered.len(),
"policies": filtered.iter().map(|r| json!({
"path": r.path,
"version": r.version,
"description": r.description,
"match_action": r.match_action,
"deny": r.deny,
"require_approval": r.require_approval,
"agent_id": r.agent_id,
})).collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
fn show(path: &std::path::Path, args: ShowArgs) -> Result<()> {
let file = PolicyFile::load(path)?;
let rule = file
.policies
.iter()
.find(|r| r.path == args.path)
.ok_or_else(|| anyhow!("policy not found: {}", args.path))?;
println!("{}", serde_json::to_string_pretty(rule)?);
Ok(())
}
fn evaluate(path: &std::path::Path, args: EvaluateArgs) -> Result<()> {
let gate = FilePolicyGate::from_file(path)?;
let situation = Situation {
description: args
.description
.unwrap_or_else(|| format!("dry-run evaluation of {}", args.action)),
qualifiers: serde_json::Value::Null,
};
let decision = gate.evaluate(&situation, &args.action, &args.agent_id)?;
let out = match &decision {
Decision::Allow { matched_policy } => json!({
"status": "allowed",
"matched_policy": matched_policy,
}),
Decision::Deny {
matched_policy,
reason,
} => json!({
"status": "denied",
"matched_policy": matched_policy,
"reason": reason,
}),
Decision::RequireApproval {
matched_policy,
approvers,
reason,
} => json!({
"status": "awaiting-approval",
"matched_policy": matched_policy,
"approvers": approvers,
"reason": reason,
}),
Decision::NoPolicyMatch => json!({
"status": "no-policy-match",
"matched_policy": serde_json::Value::Null,
}),
};
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}