agentguard/commands/
delegate.rs1use agentguard_core::auth_keys::Algorithm;
2use agentguard_core::{decode_chain_secret, DelegationConfig, DelegationSigner, DelegationToken};
3use anyhow::{anyhow, Result};
4use base64::Engine as _;
5use std::path::Path;
6use std::sync::Arc;
7use std::time::Duration;
8
9#[allow(clippy::too_many_arguments)]
10pub fn run(
11 from: &str,
12 to: &str,
13 actions: Vec<String>,
14 resources: Vec<String>,
15 ttl: i64,
16 key_id: Option<&str>,
17 key_file: Option<impl AsRef<Path>>,
18 out_path: Option<impl AsRef<Path>>,
19 output: &str,
20) -> Result<()> {
21 let signer = load_signer(key_id, key_file.as_ref().map(|p| p.as_ref()), output)?;
22 let token = signer.mint(
23 from,
24 to,
25 "agentguard://default", actions,
27 resources,
28 DelegationConfig {
29 ttl: Duration::from_secs(ttl.max(0) as u64),
30 },
31 )?;
32
33 let jws = token.to_jws().to_string();
34 if let Some(p) = out_path {
35 std::fs::write(p.as_ref(), &jws)?;
36 if output != "json" {
37 println!("wrote token to {}", p.as_ref().display());
38 }
39 } else if output == "json" {
40 println!("{}", serde_json::to_string_pretty(&token)?);
41 } else {
42 println!("{}", jws);
43 }
44 Ok(())
45}
46
47pub fn verify(token_str: &str, keys_path: impl AsRef<Path>, output: &str) -> Result<()> {
48 let path = Path::new(token_str);
49 let compact = if path.exists() && path.is_file() {
50 std::fs::read_to_string(path)?.trim().to_string()
51 } else {
52 token_str.to_string()
53 };
54 let _token = DelegationToken::parse(&compact)?;
55
56 let verifier = agentguard_core::DelegationVerifier::new();
57 let text = std::fs::read_to_string(keys_path.as_ref())?;
58 for line in text.lines() {
59 let line = line.trim();
60 if line.is_empty() || line.starts_with('#') {
61 continue;
62 }
63 let (id, key) = line
64 .split_once('=')
65 .ok_or_else(|| anyhow!("expected `kid=base64pubkey` in keys file"))?;
66 let bytes = base64::engine::general_purpose::STANDARD
67 .decode(key.trim())
68 .map_err(|e| anyhow!("base64: {}", e))?;
69 verifier
70 .add_key(id.trim(), Algorithm::EdDSA, &bytes)
71 .map_err(|e| anyhow!("add key {:?}: {}", id.trim(), e))?;
72 }
73
74 let verified = verifier
75 .verify(
76 &compact,
77 "agentguard://default",
78 chrono::Utc::now().timestamp(),
79 )
80 .map_err(|e| anyhow!("verify: {}", e))?;
81 let claims = verified.claims();
82
83 if output == "json" {
84 println!("{}", serde_json::to_string_pretty(claims)?);
85 } else {
86 println!("token valid");
87 println!(" iss: {}", claims.iss);
88 println!(" sub: {}", claims.sub);
89 println!(" aud: {}", claims.aud);
90 println!(" exp: {}", claims.exp);
91 println!(" actions: {}", claims.allowed_actions.join(", "));
92 println!(" resources: {}", claims.resource_patterns.join(", "));
93 }
94 Ok(())
95}
96
97fn load_signer(
98 key_id: Option<&str>,
99 key_file: Option<&Path>,
100 output: &str,
101) -> Result<Arc<DelegationSigner>> {
102 if let Some(p) = key_file {
103 return Ok(Arc::new(load_signer_from_file(p, key_id)?));
104 }
105 if let Some(p) = key_id {
106 if Path::new(p).exists() {
107 return Ok(Arc::new(load_signer_from_file(Path::new(p), Some(p))?));
108 }
109 let bytes = decode_chain_secret(p.as_bytes()).ok_or_else(|| anyhow!("key_id is empty"))?;
110 let mut s = DelegationSigner::from_bytes(&bytes)?;
111 if !p.is_empty() {
112 s.set_key_id(p);
113 }
114 return Ok(Arc::new(s));
115 }
116 let s = DelegationSigner::generate();
117 if output != "json" {
118 eprintln!(
119 "warning: ephemeral key — public key ({}): {}",
120 s.key_id(),
121 s.public_key_b64()
122 );
123 }
124 Ok(Arc::new(s))
125}
126
127fn load_signer_from_file(path: &Path, kid_hint: Option<&str>) -> Result<DelegationSigner> {
128 let text = std::fs::read_to_string(path)?;
129 let payload = if let Some(idx) = text.find('=') {
130 text[idx + 1..].trim().to_string()
131 } else {
132 text.trim().to_string()
133 };
134 let bytes = decode_chain_secret(payload.as_bytes())
135 .ok_or_else(|| anyhow!("signer payload is empty"))?;
136 let mut s = DelegationSigner::from_bytes(&bytes)?;
137 if let Some(k) = kid_hint {
138 if !k.is_empty() && k != payload {
139 s.set_key_id(k.to_string());
140 }
141 }
142 Ok(s)
143}
144
145#[allow(dead_code)]
146fn base64_decode(s: &str) -> Result<Vec<u8>> {
147 base64::engine::general_purpose::STANDARD
148 .decode(s)
149 .map_err(|e| anyhow!("base64: {}", e))
150}