1use clap::{Parser, Subcommand};
2
3mod commands;
4
5#[derive(Parser)]
6#[command(name = "agentguard", version, about = "Cedar-powered authorization for AI agents", long_about = None)]
7struct Cli {
8 #[arg(
10 long,
11 global = true,
12 env = "AGENTGUARD_STORE",
13 default_value = ".agentguard"
14 )]
15 store: String,
16
17 #[arg(
19 long,
20 global = true,
21 env = "AGENTGUARD_AUDIT",
22 default_value = ".audit/decisions.jsonl"
23 )]
24 audit: String,
25
26 #[arg(long, global = true, default_value = "pretty")]
28 output: String,
29
30 #[arg(long, global = true, env = "AGENTGUARD_CHAIN_SECRET")]
34 secret_file: Option<String>,
35
36 #[command(subcommand)]
37 cmd: Cmd,
38}
39
40#[derive(Subcommand)]
41enum Cmd {
42 Init {
44 #[arg(long, default_value = "myorg")]
46 name: String,
47 },
48 Validate,
50 Authorize {
52 #[arg(default_value = "-")]
54 request: String,
55 #[arg(long)]
57 entities: Option<String>,
58 #[arg(long, alias = "no-audit")]
61 skip_audit: bool,
62 },
63 Sim {
65 #[arg(default_value = "-")]
66 request: String,
67 #[arg(long)]
68 entities: Option<String>,
69 },
70 Delegate {
72 #[arg(long)]
73 from: String,
74 #[arg(long)]
75 to: String,
76 #[arg(long)]
77 actions: Vec<String>,
78 #[arg(long)]
79 resources: Vec<String>,
80 #[arg(long, default_value_t = 900)]
81 ttl: i64,
82 #[arg(long)]
83 key_id: Option<String>,
84 #[arg(long)]
85 key_file: Option<String>,
86 #[arg(long)]
88 out: Option<String>,
89 },
90 Verify {
92 token: String,
94 #[arg(long)]
96 keys: String,
97 },
98 ApiKey {
100 #[command(subcommand)]
101 action: ApiKeyCmd,
102 },
103 Schema,
105 Log {
107 #[command(subcommand)]
108 action: LogCmd,
109 },
110 Audit {
112 #[command(subcommand)]
113 action: AuditCmd,
114 },
115 Gen {
117 description: String,
118 #[arg(long)]
120 name: Option<String>,
121 #[arg(long, default_value = "openai")]
123 provider: String,
124 #[arg(long, default_value = "gpt-4o-mini")]
126 model: String,
127 #[arg(long)]
131 dry_run: bool,
132 #[arg(long)]
135 confirm: bool,
136 },
137 Doctor,
139}
140
141#[derive(Subcommand)]
142enum ApiKeyCmd {
143 Create {
145 #[arg(long)]
147 key_store: String,
148 #[arg(long, default_value = "ag_live")]
149 prefix: String,
150 #[arg(long, value_parser = ["User", "Agent"])]
151 subject_type: String,
152 #[arg(long)]
153 subject_id: String,
154 #[arg(long)]
155 tenant_id: Option<String>,
156 #[arg(long = "scope", default_value = "authorize")]
158 scopes: Vec<String>,
159 #[arg(long, default_value_t = 2_592_000)]
161 ttl_seconds: u64,
162 },
163 List {
165 #[arg(long)]
166 key_store: String,
167 },
168 Revoke {
170 #[arg(long)]
171 key_store: String,
172 id: String,
173 },
174}
175
176#[derive(Subcommand)]
177enum LogCmd {
178 Tail {
180 #[arg(long, default_value_t = 20)]
181 n: usize,
182 #[arg(long)]
184 principal: Option<String>,
185 #[arg(long)]
187 action: Option<String>,
188 },
189 Dump,
191}
192
193#[derive(Subcommand)]
194enum AuditCmd {
195 Verify {
197 #[arg(long)]
199 audit: Option<String>,
200 #[arg(long)]
202 secret_file: String,
203 },
204 Export {
206 #[arg(long)]
208 audit: Option<String>,
209 #[arg(long, default_value = "ecs")]
211 format: String,
212 #[arg(long)]
214 out: Option<String>,
215 },
216 Sar {
218 #[arg(long)]
220 audit: Option<String>,
221 subject_id: String,
223 },
224 Erase {
226 #[arg(long)]
228 audit: Option<String>,
229 subject_id: String,
231 #[arg(long)]
233 salt_file: String,
234 #[arg(long)]
236 out: Option<String>,
237 },
238 Rotate {
243 #[arg(long)]
245 audit: Option<String>,
246 #[arg(long)]
250 secret_file: Option<String>,
251 },
252}
253
254#[tokio::main(flavor = "current_thread")]
255async fn main() {
256 let exit_code = run().await;
257 std::process::exit(exit_code);
260}
261
262async fn run() -> i32 {
263 tracing_subscriber::fmt()
264 .with_env_filter(
265 tracing_subscriber::EnvFilter::try_from_default_env()
266 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn,agentguard=info")),
267 )
268 .init();
269
270 let cli = Cli::parse();
271 let out = cli.output.as_str();
272
273 let mut exit_code: i32 = 0;
274 let res: anyhow::Result<()> = match cli.cmd {
275 Cmd::Init { name } => commands::init::run(&cli.store, &name),
276 Cmd::Validate => commands::validate::run(&cli.store, out),
277 Cmd::Authorize {
278 request,
279 entities,
280 skip_audit,
281 } => {
282 let outcome = match commands::authorize::run(
283 &cli.store,
284 &cli.audit,
285 &request,
286 entities.as_deref(),
287 skip_audit,
288 out,
289 cli.secret_file.as_deref().map(std::path::Path::new),
290 )
291 .await
292 {
293 Ok(o) => o,
294 Err(e) => {
295 eprintln!("error: {e:?}");
296 return 1;
297 }
298 };
299 if !outcome.decision_was_allow {
303 exit_code = 2;
304 }
305 Ok::<(), anyhow::Error>(())
306 }
307 Cmd::Sim { request, entities } => {
308 commands::sim::run(&cli.store, &request, entities.as_deref(), out).await
309 }
310 Cmd::Delegate {
311 from,
312 to,
313 actions,
314 resources,
315 ttl,
316 key_id,
317 key_file,
318 out: out_path,
319 } => commands::delegate::run(
320 &from,
321 &to,
322 actions,
323 resources,
324 ttl,
325 key_id.as_deref(),
326 key_file.as_deref(),
327 out_path.as_deref(),
328 out,
329 ),
330 Cmd::Verify { token, keys } => commands::delegate::verify(&token, &keys, out),
331 Cmd::ApiKey { action } => match action {
332 ApiKeyCmd::Create {
333 key_store,
334 prefix,
335 subject_type,
336 subject_id,
337 tenant_id,
338 scopes,
339 ttl_seconds,
340 } => commands::apikey::create(
341 commands::apikey::CreateKeyOptions {
342 path: std::path::Path::new(&key_store),
343 prefix: &prefix,
344 subject_type: &subject_type,
345 subject_id: &subject_id,
346 tenant_id: tenant_id.as_deref(),
347 scopes,
348 ttl_seconds,
349 },
350 out,
351 ),
352 ApiKeyCmd::List { key_store } => commands::apikey::list(&key_store, out),
353 ApiKeyCmd::Revoke { key_store, id } => commands::apikey::revoke(&key_store, &id, out),
354 },
355 Cmd::Schema => commands::schema::run(&cli.store, out),
356 Cmd::Log { action } => match action {
357 LogCmd::Tail {
358 n,
359 principal,
360 action,
361 } => commands::log::tail(&cli.audit, n, principal.as_deref(), action.as_deref(), out),
362 LogCmd::Dump => commands::log::dump(&cli.audit, out),
363 },
364 Cmd::Gen {
365 description,
366 name,
367 provider,
368 model,
369 dry_run,
370 confirm,
371 } => {
372 commands::gen::run(
373 &cli.store,
374 &description,
375 name.as_deref(),
376 &provider,
377 &model,
378 dry_run,
379 confirm,
380 out,
381 )
382 .await
383 }
384 Cmd::Audit { action } => match action {
385 AuditCmd::Verify { audit, secret_file } => {
386 let path = audit.as_deref().unwrap_or(&cli.audit);
387 commands::audit::verify(path, &secret_file, out)
388 }
389 AuditCmd::Export {
390 audit,
391 format,
392 out: out_path,
393 } => {
394 let path = audit.as_deref().unwrap_or(&cli.audit);
395 commands::audit::export(path, &format, out_path.as_deref(), out)
396 }
397 AuditCmd::Sar { audit, subject_id } => {
398 let path = audit.as_deref().unwrap_or(&cli.audit);
399 commands::audit::sar(path, &subject_id, out)
400 }
401 AuditCmd::Erase {
402 audit,
403 subject_id,
404 salt_file,
405 out: out_path,
406 } => {
407 let path = audit.as_deref().unwrap_or(&cli.audit);
408 commands::audit::erase(path, &subject_id, &salt_file, out_path.as_deref())
409 }
410 AuditCmd::Rotate { audit, secret_file } => {
411 let path = audit.as_deref().unwrap_or(&cli.audit);
412 let secret = secret_file
413 .as_deref()
414 .or(cli.secret_file.as_deref())
415 .map(std::path::Path::new);
416 commands::audit::rotate(path, secret)
417 }
418 },
419 Cmd::Doctor => {
420 let chain_secret = cli.secret_file.as_deref().map(std::path::Path::new);
424 let report = match commands::doctor::run(
425 std::path::Path::new(&cli.store),
426 std::path::Path::new(&cli.audit),
427 chain_secret,
428 ) {
429 Ok(r) => r,
430 Err(e) => {
431 eprintln!("error: {e:?}");
432 return 1;
433 }
434 };
435 report.print();
436 exit_code = if report.has_failures() {
437 1
438 } else if report.has_warnings() {
439 2
440 } else {
441 0
442 };
443 Ok::<(), anyhow::Error>(())
444 }
445 };
446
447 if let Err(e) = res {
448 eprintln!("error: {e:?}");
449 if exit_code == 0 {
450 exit_code = 1;
451 }
452 }
453 exit_code
454}