auths_cli/commands/id/
agent.rs1use 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#[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#[derive(Subcommand, Debug, Clone)]
36pub enum AgentSubcommand {
37 Add {
39 #[arg(long, help = "Label / keychain alias for the new agent key.")]
41 label: String,
42
43 #[arg(long, help = "Your root identity's signing key name (the delegator).")]
45 key: String,
46
47 #[arg(
49 long,
50 default_value = "p256",
51 help = "Curve for the new agent key (p256 or ed25519)."
52 )]
53 curve: String,
54
55 #[arg(long = "scope", help = "Capability to grant the agent (repeatable).")]
57 scope: Vec<String>,
58
59 #[arg(long = "expires-in", help = "Expire the agent after N seconds.")]
61 expires_in: Option<i64>,
62 },
63
64 Rotate {
66 #[arg(help = "The agent's did:keri to rotate.")]
68 agent_did: String,
69
70 #[arg(long, help = "Your root identity's signing key name (the delegator).")]
72 key: String,
73 },
74
75 Revoke {
77 #[arg(help = "The agent's did:keri to revoke.")]
79 agent_did: String,
80
81 #[arg(long, help = "Your root identity's signing key name (the delegator).")]
83 key: String,
84 },
85
86 List {
88 #[arg(long, help = "Include revoked agents.")]
90 include_revoked: bool,
91 },
92}
93
94#[derive(Debug, Serialize)]
96struct AgentAddResponse {
97 agent_did: String,
98 agent_prefix: String,
99}
100
101pub 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 #[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
226fn 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}