1use 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 (delegated identities under your root).",
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<auths_keri::Capability>,
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 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 under your identity:");
158 println!(" {}", crate::ux::product_id(&result.agent_did));
159 println!(
160 "\nYour root identity authorized this agent and recorded it in your tamper-evident history."
161 );
162 }
163 Ok(())
164 }
165
166 AgentSubcommand::Rotate { agent_did, key } => {
167 let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
168 let root_alias = KeyAlias::new_unchecked(key);
169 auths_sdk::domains::agents::rotate(&ctx, &root_alias, &agent_did)
170 .map_err(anyhow::Error::new)?;
171 let _ = auths_sdk::workflows::commit_hooks::refresh_commit_trailers(&ctx, &repo_path);
174
175 if is_json_mode() {
176 JsonResponse::success(
177 "id agent rotate",
178 serde_json::json!({ "agent_did": agent_did, "rotated": true }),
179 )
180 .print()?;
181 } else {
182 println!(
183 "✓ Agent key rotated (recorded by your root identity): {}",
184 crate::ux::product_id(&agent_did)
185 );
186 }
187 Ok(())
188 }
189
190 AgentSubcommand::Revoke { agent_did, key } => {
191 let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
192 let root_alias = KeyAlias::new_unchecked(key);
193 auths_sdk::domains::agents::revoke(&ctx, &root_alias, &agent_did)
194 .map_err(anyhow::Error::new)?;
195 let _ = auths_sdk::workflows::commit_hooks::refresh_commit_trailers(&ctx, &repo_path);
198
199 if is_json_mode() {
200 JsonResponse::success(
201 "id agent revoke",
202 serde_json::json!({ "agent_did": agent_did, "revoked": true }),
203 )
204 .print()?;
205 } else {
206 println!(
207 "✓ Agent revoked (recorded in your tamper-evident history): {}",
208 crate::ux::product_id(&agent_did)
209 );
210 }
211 Ok(())
212 }
213
214 AgentSubcommand::List { include_revoked } => {
215 let ctx = build_auths_context(&repo_path, env_config, Some(passphrase_provider))?;
216 let agents = auths_sdk::domains::agents::list(&ctx).map_err(anyhow::Error::new)?;
217 let shown: Vec<_> = agents
218 .into_iter()
219 .filter(|a| include_revoked || !a.revoked)
220 .collect();
221
222 if is_json_mode() {
223 let data: Vec<_> = shown
224 .iter()
225 .map(|a| serde_json::json!({ "agent_did": a.agent_did, "revoked": a.revoked }))
226 .collect();
227 JsonResponse::success("id agent list", serde_json::json!({ "agents": data }))
228 .print()?;
229 } else if shown.is_empty() {
230 println!("No agents delegated by this identity.");
231 } else {
232 println!("Delegated agents:");
233 for a in &shown {
234 let status = if a.revoked { " (revoked)" } else { "" };
235 println!(" {}{}", crate::ux::product_id(&a.agent_did), status);
236 }
237 }
238 Ok(())
239 }
240 }
241}
242
243fn parse_curve(s: &str) -> Result<auths_crypto::CurveType> {
245 match s.to_ascii_lowercase().as_str() {
246 "p256" | "p-256" => Ok(auths_crypto::CurveType::P256),
247 "ed25519" => Ok(auths_crypto::CurveType::Ed25519),
248 other => Err(anyhow::anyhow!(
249 "unknown curve {:?}: expected p256 or ed25519",
250 other
251 )),
252 }
253}