Skip to main content

auths_cli/commands/id/
agent.rs

1//! `auths id agent …` — manage AI agents as KERI delegated identifiers.
2//!
3//! An agent is a KERI delegated AID (`dip` delegated by your root identity, anchored
4//! by the root's `ixn`) — the same mechanism as a delegated device, not a bearer
5//! token or a standalone identity. This is the thin presentation layer; all business
6//! logic lives in `auths_sdk::domains::agents`.
7
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use anyhow::Result;
12use clap::{Parser, Subcommand};
13use serde::Serialize;
14
15use auths_sdk::core_config::EnvironmentConfig;
16use auths_sdk::keychain::KeyAlias;
17use auths_sdk::signing::PassphraseProvider;
18
19use crate::factories::storage::build_auths_context;
20use crate::ux::format::{JsonResponse, is_json_mode};
21
22/// Manage AI agents delegated by your identity.
23#[derive(Parser, Debug, Clone)]
24#[command(
25    about = "Manage AI agents (KERI delegated identifiers).",
26    after_help = "Examples:
27  auths id agent add --label deploy-bot --key my-key   # Delegate a new agent"
28)]
29pub struct AgentCommand {
30    #[clap(subcommand)]
31    pub subcommand: AgentSubcommand,
32}
33
34/// Agent subcommands.
35#[derive(Subcommand, Debug, Clone)]
36pub enum AgentSubcommand {
37    /// Delegate a new agent as a KERI delegated identifier of your root identity.
38    Add {
39        /// Label — also the keychain alias the new agent key is stored under.
40        #[arg(long, help = "Label / keychain alias for the new agent key.")]
41        label: String,
42
43        /// Your root identity's signing key name (the delegator).
44        #[arg(long, help = "Your root identity's signing key name (the delegator).")]
45        key: String,
46
47        /// Curve for the new agent key (defaults to P-256, the project default).
48        #[arg(
49            long,
50            default_value = "p256",
51            help = "Curve for the new agent key (p256 or ed25519)."
52        )]
53        curve: String,
54
55        /// Capability to grant the agent (repeatable). Empty = unrestricted.
56        #[arg(long = "scope", help = "Capability to grant the agent (repeatable).")]
57        scope: Vec<String>,
58
59        /// Expire the agent this many seconds from now (delegator-anchored).
60        #[arg(long = "expires-in", help = "Expire the agent after N seconds.")]
61        expires_in: Option<i64>,
62    },
63
64    /// Rotate a delegated agent's key (`drt`), anchored by your root identity.
65    Rotate {
66        /// The agent's `did:keri:` to rotate.
67        #[arg(help = "The agent's did:keri to rotate.")]
68        agent_did: String,
69
70        /// Your root identity's signing key name (the delegator that anchors the rotation).
71        #[arg(long, help = "Your root identity's signing key name (the delegator).")]
72        key: String,
73    },
74
75    /// Revoke a delegated agent (anchors a revocation seal in your root's KEL).
76    Revoke {
77        /// The agent's `did:keri:` to revoke.
78        #[arg(help = "The agent's did:keri to revoke.")]
79        agent_did: String,
80
81        /// Your root identity's signing key name (the delegator).
82        #[arg(long, help = "Your root identity's signing key name (the delegator).")]
83        key: String,
84    },
85
86    /// List the agents this identity has delegated (excludes devices).
87    List {
88        /// Include revoked agents in the listing.
89        #[arg(long, help = "Include revoked agents.")]
90        include_revoked: bool,
91    },
92}
93
94/// JSON response for `id agent add`.
95#[derive(Debug, Serialize)]
96struct AgentAddResponse {
97    agent_did: String,
98    agent_prefix: String,
99}
100
101/// Dispatch an `auths id agent …` subcommand.
102///
103/// Args:
104/// * `cmd`: The parsed agent command.
105/// * `repo_path`: Resolved registry repository path.
106/// * `env_config`: Environment configuration for context building.
107/// * `passphrase_provider`: Passphrase source for key access.
108///
109/// Usage:
110/// ```ignore
111/// handle_agent(cmd, repo_path, &env_config, passphrase_provider)?;
112/// ```
113pub fn handle_agent(
114    cmd: AgentCommand,
115    repo_path: PathBuf,
116    env_config: &EnvironmentConfig,
117    passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
118) -> Result<()> {
119    match cmd.subcommand {
120        AgentSubcommand::Add {
121            label,
122            key,
123            curve,
124            scope,
125            expires_in,
126        } => {
127            let curve = parse_curve(&curve)?;
128            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
129            let root_alias = KeyAlias::new_unchecked(key);
130            let agent_alias = KeyAlias::new_unchecked(label);
131            // Clock at the presentation boundary (the SDK/core never call Utc::now()).
132            #[allow(clippy::disallowed_methods)]
133            let expires_at = expires_in.map(|secs| chrono::Utc::now().timestamp() + secs);
134            let result = auths_sdk::domains::agents::add_scoped(
135                &ctx,
136                &root_alias,
137                &agent_alias,
138                curve,
139                &scope,
140                expires_at,
141            )
142            .map_err(anyhow::Error::new)?;
143
144            if is_json_mode() {
145                JsonResponse::success(
146                    "id agent add",
147                    AgentAddResponse {
148                        agent_did: result.agent_did.clone(),
149                        agent_prefix: result.agent_prefix.clone(),
150                    },
151                )
152                .print()?;
153            } else {
154                println!("✓ Agent delegated as a KERI delegated identifier:");
155                println!("  {}", result.agent_did);
156                println!("\nThe root anchored this agent's delegation in its KEL.");
157            }
158            Ok(())
159        }
160
161        AgentSubcommand::Rotate { agent_did, key } => {
162            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
163            let root_alias = KeyAlias::new_unchecked(key);
164            auths_sdk::domains::agents::rotate(&ctx, &root_alias, &agent_did)
165                .map_err(anyhow::Error::new)?;
166
167            if is_json_mode() {
168                JsonResponse::success(
169                    "id agent rotate",
170                    serde_json::json!({ "agent_did": agent_did, "rotated": true }),
171                )
172                .print()?;
173            } else {
174                println!("✓ Agent key rotated (drt anchored by the root): {agent_did}");
175            }
176            Ok(())
177        }
178
179        AgentSubcommand::Revoke { agent_did, key } => {
180            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
181            let root_alias = KeyAlias::new_unchecked(key);
182            auths_sdk::domains::agents::revoke(&ctx, &root_alias, &agent_did)
183                .map_err(anyhow::Error::new)?;
184
185            if is_json_mode() {
186                JsonResponse::success(
187                    "id agent revoke",
188                    serde_json::json!({ "agent_did": agent_did, "revoked": true }),
189                )
190                .print()?;
191            } else {
192                println!("✓ Agent revoked (revocation anchored in the root KEL): {agent_did}");
193            }
194            Ok(())
195        }
196
197        AgentSubcommand::List { include_revoked } => {
198            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
199            let agents = auths_sdk::domains::agents::list(&ctx).map_err(anyhow::Error::new)?;
200            let shown: Vec<_> = agents
201                .into_iter()
202                .filter(|a| include_revoked || !a.revoked)
203                .collect();
204
205            if is_json_mode() {
206                let data: Vec<_> = shown
207                    .iter()
208                    .map(|a| serde_json::json!({ "agent_did": a.agent_did, "revoked": a.revoked }))
209                    .collect();
210                JsonResponse::success("id agent list", serde_json::json!({ "agents": data }))
211                    .print()?;
212            } else if shown.is_empty() {
213                println!("No agents delegated by this identity.");
214            } else {
215                println!("Delegated agents:");
216                for a in &shown {
217                    let status = if a.revoked { " (revoked)" } else { "" };
218                    println!("  {}{}", a.agent_did, status);
219                }
220            }
221            Ok(())
222        }
223    }
224}
225
226/// Parse a curve name (`ed25519` / `p256`) into a [`CurveType`](auths_crypto::CurveType).
227fn parse_curve(s: &str) -> Result<auths_crypto::CurveType> {
228    match s.to_ascii_lowercase().as_str() {
229        "p256" | "p-256" => Ok(auths_crypto::CurveType::P256),
230        "ed25519" => Ok(auths_crypto::CurveType::Ed25519),
231        other => Err(anyhow::anyhow!(
232            "unknown curve {:?}: expected p256 or ed25519",
233            other
234        )),
235    }
236}