agentstatedeveloper_cli/commands/
policy.rs1use anyhow::{Context, Result, anyhow};
8use clap::{Args, Subcommand};
9use serde_json::json;
10
11use agentstatedeveloper_core::{Decision, FilePolicyGate, PolicyFile, PolicyGate, Situation};
12
13use crate::config::Config;
14
15#[derive(Debug, Subcommand)]
16pub enum PolicyCmd {
17 List(ListArgs),
19
20 Show(ShowArgs),
22
23 Evaluate(EvaluateArgs),
26}
27
28#[derive(Debug, Args)]
29pub struct ListArgs {
30 #[arg(long)]
32 pub prefix: Option<String>,
33}
34
35#[derive(Debug, Args)]
36pub struct ShowArgs {
37 pub path: String,
40}
41
42#[derive(Debug, Args)]
43pub struct EvaluateArgs {
44 pub action: String,
46
47 #[arg(long, default_value = "asd-cli-user")]
49 pub agent_id: String,
50
51 #[arg(long)]
53 pub description: Option<String>,
54}
55
56pub fn run(cfg: &Config, cmd: PolicyCmd) -> Result<()> {
57 let path = cfg.policy_path.as_ref().ok_or_else(|| {
58 anyhow!("no policy file configured — pass --policy <path> or set ASD_POLICY")
59 })?;
60
61 match cmd {
62 PolicyCmd::List(args) => list(path, args),
63 PolicyCmd::Show(args) => show(path, args),
64 PolicyCmd::Evaluate(args) => evaluate(path, args),
65 }
66}
67
68fn list(path: &std::path::Path, args: ListArgs) -> Result<()> {
69 let file =
70 PolicyFile::load(path).with_context(|| format!("load policy file {}", path.display()))?;
71 let filtered: Vec<_> = file
72 .policies
73 .iter()
74 .filter(|r| args.prefix.as_deref().is_none_or(|p| r.path.starts_with(p)))
75 .collect();
76
77 let out = json!({
78 "source": path.display().to_string(),
79 "strict": file.strict,
80 "count": filtered.len(),
81 "policies": filtered.iter().map(|r| json!({
82 "path": r.path,
83 "version": r.version,
84 "description": r.description,
85 "match_action": r.match_action,
86 "deny": r.deny,
87 "require_approval": r.require_approval,
88 "agent_id": r.agent_id,
89 })).collect::<Vec<_>>(),
90 });
91 println!("{}", serde_json::to_string_pretty(&out)?);
92 Ok(())
93}
94
95fn show(path: &std::path::Path, args: ShowArgs) -> Result<()> {
96 let file = PolicyFile::load(path)?;
97 let rule = file
98 .policies
99 .iter()
100 .find(|r| r.path == args.path)
101 .ok_or_else(|| anyhow!("policy not found: {}", args.path))?;
102 println!("{}", serde_json::to_string_pretty(rule)?);
103 Ok(())
104}
105
106fn evaluate(path: &std::path::Path, args: EvaluateArgs) -> Result<()> {
107 let gate = FilePolicyGate::from_file(path)?;
108 let situation = Situation {
109 description: args
110 .description
111 .unwrap_or_else(|| format!("dry-run evaluation of {}", args.action)),
112 qualifiers: serde_json::Value::Null,
113 };
114 let decision = gate.evaluate(&situation, &args.action, &args.agent_id)?;
115
116 let out = match &decision {
117 Decision::Allow { matched_policy } => json!({
118 "status": "allowed",
119 "matched_policy": matched_policy,
120 }),
121 Decision::Deny {
122 matched_policy,
123 reason,
124 } => json!({
125 "status": "denied",
126 "matched_policy": matched_policy,
127 "reason": reason,
128 }),
129 Decision::RequireApproval {
130 matched_policy,
131 approvers,
132 reason,
133 } => json!({
134 "status": "awaiting-approval",
135 "matched_policy": matched_policy,
136 "approvers": approvers,
137 "reason": reason,
138 }),
139 Decision::NoPolicyMatch => json!({
140 "status": "no-policy-match",
141 "matched_policy": serde_json::Value::Null,
142 }),
143 };
144 println!("{}", serde_json::to_string_pretty(&out)?);
145 Ok(())
146}