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<auths_keri::Capability>,
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            // The delegation advanced the root KEL — restamp the trailer file's
144            // Auths-Anchor-Seq (best-effort; doctor surfaces a stale hook setup).
145            let _ = auths_sdk::workflows::commit_hooks::refresh_commit_trailers(&ctx, &repo_path);
146
147            if is_json_mode() {
148                JsonResponse::success(
149                    "id agent add",
150                    AgentAddResponse {
151                        agent_did: result.agent_did.clone(),
152                        agent_prefix: result.agent_prefix.clone(),
153                    },
154                )
155                .print()?;
156            } else {
157                println!("✓ Agent delegated as a KERI delegated identifier:");
158                println!("  {}", result.agent_did);
159                println!("\nThe root anchored this agent's delegation in its KEL.");
160            }
161            Ok(())
162        }
163
164        AgentSubcommand::Rotate { agent_did, key } => {
165            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
166            let root_alias = KeyAlias::new_unchecked(key);
167            auths_sdk::domains::agents::rotate(&ctx, &root_alias, &agent_did)
168                .map_err(anyhow::Error::new)?;
169            // The delegation advanced the root KEL — restamp the trailer file's
170            // Auths-Anchor-Seq (best-effort; doctor surfaces a stale hook setup).
171            let _ = auths_sdk::workflows::commit_hooks::refresh_commit_trailers(&ctx, &repo_path);
172
173            if is_json_mode() {
174                JsonResponse::success(
175                    "id agent rotate",
176                    serde_json::json!({ "agent_did": agent_did, "rotated": true }),
177                )
178                .print()?;
179            } else {
180                println!("✓ Agent key rotated (drt anchored by the root): {agent_did}");
181            }
182            Ok(())
183        }
184
185        AgentSubcommand::Revoke { agent_did, key } => {
186            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
187            let root_alias = KeyAlias::new_unchecked(key);
188            auths_sdk::domains::agents::revoke(&ctx, &root_alias, &agent_did)
189                .map_err(anyhow::Error::new)?;
190            // The delegation advanced the root KEL — restamp the trailer file's
191            // Auths-Anchor-Seq (best-effort; doctor surfaces a stale hook setup).
192            let _ = auths_sdk::workflows::commit_hooks::refresh_commit_trailers(&ctx, &repo_path);
193
194            if is_json_mode() {
195                JsonResponse::success(
196                    "id agent revoke",
197                    serde_json::json!({ "agent_did": agent_did, "revoked": true }),
198                )
199                .print()?;
200            } else {
201                println!("✓ Agent revoked (revocation anchored in the root KEL): {agent_did}");
202            }
203            Ok(())
204        }
205
206        AgentSubcommand::List { include_revoked } => {
207            let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
208            let agents = auths_sdk::domains::agents::list(&ctx).map_err(anyhow::Error::new)?;
209            let shown: Vec<_> = agents
210                .into_iter()
211                .filter(|a| include_revoked || !a.revoked)
212                .collect();
213
214            if is_json_mode() {
215                let data: Vec<_> = shown
216                    .iter()
217                    .map(|a| serde_json::json!({ "agent_did": a.agent_did, "revoked": a.revoked }))
218                    .collect();
219                JsonResponse::success("id agent list", serde_json::json!({ "agents": data }))
220                    .print()?;
221            } else if shown.is_empty() {
222                println!("No agents delegated by this identity.");
223            } else {
224                println!("Delegated agents:");
225                for a in &shown {
226                    let status = if a.revoked { " (revoked)" } else { "" };
227                    println!("  {}{}", a.agent_did, status);
228                }
229            }
230            Ok(())
231        }
232    }
233}
234
235/// Parse a curve name (`ed25519` / `p256`) into a [`CurveType`](auths_crypto::CurveType).
236fn parse_curve(s: &str) -> Result<auths_crypto::CurveType> {
237    match s.to_ascii_lowercase().as_str() {
238        "p256" | "p-256" => Ok(auths_crypto::CurveType::P256),
239        "ed25519" => Ok(auths_crypto::CurveType::Ed25519),
240        other => Err(anyhow::anyhow!(
241            "unknown curve {:?}: expected p256 or ed25519",
242            other
243        )),
244    }
245}