agentguard/commands/
delegate.rs

1use 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", // v2: required audience (RFC 8707)
26        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}