1use crate::cli::CliOutput;
8use crate::cli::helpers::id_short;
9use crate::{db, identity, validate};
10use anyhow::Result;
11use clap::{Args, Subcommand};
12use std::path::Path;
13
14#[derive(Args)]
15pub struct AgentsArgs {
16 #[command(subcommand)]
17 pub action: Option<AgentsAction>,
18}
19
20#[derive(Subcommand)]
21pub enum AgentsAction {
22 List,
24 Register {
26 #[arg(long)]
28 agent_id: String,
29 #[arg(long)]
34 agent_type: String,
35 #[arg(long, default_value = "")]
37 capabilities: String,
38 },
39}
40
41#[derive(Args)]
42pub struct PendingArgs {
43 #[command(subcommand)]
44 pub action: PendingAction,
45}
46
47#[derive(Subcommand)]
48pub enum PendingAction {
49 List {
51 #[arg(long)]
52 status: Option<String>,
53 #[arg(long, default_value_t = 100)]
54 limit: usize,
55 },
56 Approve { id: String },
58 Reject { id: String },
60}
61
62pub fn run_agents(
64 db_path: &Path,
65 args: AgentsArgs,
66 json_out: bool,
67 out: &mut CliOutput<'_>,
68) -> Result<()> {
69 let conn = db::open(db_path)?;
70 match args.action.unwrap_or(AgentsAction::List) {
71 AgentsAction::List => {
72 let agents = db::list_agents(&conn)?;
73 if json_out {
74 writeln!(
75 out.stdout,
76 "{}",
77 serde_json::json!({"count": agents.len(), "agents": agents})
78 )?;
79 } else if agents.is_empty() {
80 writeln!(out.stdout, "no registered agents")?;
81 } else {
82 for a in &agents {
83 let caps = if a.capabilities.is_empty() {
84 String::new()
85 } else {
86 format!(" [{}]", a.capabilities.join(","))
87 };
88 writeln!(
89 out.stdout,
90 "{} type={} registered={} last_seen={}{}",
91 a.agent_id, a.agent_type, a.registered_at, a.last_seen_at, caps
92 )?;
93 }
94 writeln!(out.stdout, "{} registered agents", agents.len())?;
95 }
96 }
97 AgentsAction::Register {
98 agent_id,
99 agent_type,
100 capabilities,
101 } => {
102 validate::validate_agent_id(&agent_id)?;
103 validate::validate_agent_type(&agent_type)?;
104 let caps: Vec<String> = capabilities
105 .split(',')
106 .map(str::trim)
107 .filter(|s| !s.is_empty())
108 .map(String::from)
109 .collect();
110 validate::validate_capabilities(&caps)?;
111 let id = db::register_agent(&conn, &agent_id, &agent_type, &caps)?;
112 if json_out {
113 writeln!(
114 out.stdout,
115 "{}",
116 serde_json::json!({
117 "registered": true,
118 "id": id,
119 "agent_id": agent_id,
120 "agent_type": agent_type,
121 "capabilities": caps,
122 })
123 )?;
124 } else {
125 writeln!(
126 out.stdout,
127 "registered {agent_id} (type={agent_type}, capabilities={})",
128 if caps.is_empty() {
129 "-".to_string()
130 } else {
131 caps.join(",")
132 }
133 )?;
134 }
135 }
136 }
137 Ok(())
138}
139
140pub fn run_pending(
142 db_path: &Path,
143 args: PendingArgs,
144 json_out: bool,
145 cli_agent_id: Option<&str>,
146 out: &mut CliOutput<'_>,
147) -> Result<()> {
148 let conn = db::open(db_path)?;
149 match args.action {
150 PendingAction::List { status, limit } => {
151 let items = db::list_pending_actions(&conn, status.as_deref(), limit)?;
152 if json_out {
153 writeln!(
154 out.stdout,
155 "{}",
156 serde_json::json!({"count": items.len(), "pending": items})
157 )?;
158 } else if items.is_empty() {
159 writeln!(out.stdout, "no pending actions")?;
160 } else {
161 for item in &items {
162 writeln!(
163 out.stdout,
164 "[{}] {} ns={} action={} by={} ({})",
165 id_short(&item.id),
166 item.status,
167 item.namespace,
168 item.action_type,
169 item.requested_by,
170 item.requested_at
171 )?;
172 }
173 writeln!(out.stdout, "{} pending action(s)", items.len())?;
174 }
175 }
176 PendingAction::Approve { id } => {
177 use db::ApproveOutcome;
178 validate::validate_id(&id)?;
179 let agent = identity::resolve_agent_id(cli_agent_id, None)?;
180 match db::approve_with_approver_type(&conn, &id, &agent)? {
181 ApproveOutcome::Approved => {
182 let executed = db::execute_pending_action(&conn, &id)?;
183 if json_out {
184 writeln!(
185 out.stdout,
186 "{}",
187 serde_json::json!({
188 "approved": true,
189 "id": id,
190 "decided_by": agent,
191 "executed": true,
192 "memory_id": executed,
193 })
194 )?;
195 } else {
196 writeln!(out.stdout, "approved + executed: {id} (by {agent})")?;
197 }
198 }
199 ApproveOutcome::Pending { votes, quorum } => {
200 if json_out {
201 writeln!(
202 out.stdout,
203 "{}",
204 serde_json::json!({
205 "approved": false,
206 "status": "pending",
207 "id": id,
208 "votes": votes,
209 "quorum": quorum,
210 "reason": "consensus threshold not yet reached",
211 })
212 )?;
213 } else {
214 writeln!(
215 out.stdout,
216 "approval recorded: {id} ({votes}/{quorum} consensus, not yet met)"
217 )?;
218 }
219 }
220 ApproveOutcome::Rejected(reason) => {
221 writeln!(out.stderr, "approve rejected: {reason}")?;
222 std::process::exit(1);
223 }
224 }
225 }
226 PendingAction::Reject { id } => {
227 validate::validate_id(&id)?;
228 let agent = identity::resolve_agent_id(cli_agent_id, None)?;
229 let ok = db::decide_pending_action(&conn, &id, false, &agent)?;
230 if !ok {
231 writeln!(
232 out.stderr,
233 "pending action not found or already decided: {id}"
234 )?;
235 std::process::exit(1);
236 }
237 if json_out {
238 writeln!(
239 out.stdout,
240 "{}",
241 serde_json::json!({"rejected": true, "id": id, "decided_by": agent})
242 )?;
243 } else {
244 writeln!(out.stdout, "rejected: {id} (by {agent})")?;
245 }
246 }
247 }
248 Ok(())
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::cli::test_utils::TestEnv;
255
256 #[test]
257 fn test_agents_list_empty() {
258 let mut env = TestEnv::fresh();
259 let db = env.db_path.clone();
260 let args = AgentsArgs {
261 action: Some(AgentsAction::List),
262 };
263 {
264 let mut out = env.output();
265 run_agents(&db, args, false, &mut out).unwrap();
266 }
267 assert!(env.stdout_str().contains("no registered agents"));
268 }
269
270 #[test]
271 fn test_agents_list_empty_json() {
272 let mut env = TestEnv::fresh();
273 let db = env.db_path.clone();
274 let args = AgentsArgs {
275 action: Some(AgentsAction::List),
276 };
277 {
278 let mut out = env.output();
279 run_agents(&db, args, true, &mut out).unwrap();
280 }
281 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
282 assert_eq!(v["count"].as_u64().unwrap(), 0);
283 }
284
285 #[test]
286 fn test_agents_register_happy_path() {
287 let mut env = TestEnv::fresh();
288 let db = env.db_path.clone();
289 let args = AgentsArgs {
290 action: Some(AgentsAction::Register {
291 agent_id: "agent-1".to_string(),
292 agent_type: "human".to_string(),
293 capabilities: "alpha,beta".to_string(),
294 }),
295 };
296 {
297 let mut out = env.output();
298 run_agents(&db, args, false, &mut out).unwrap();
299 }
300 assert!(env.stdout_str().contains("registered agent-1"));
301 }
302
303 #[test]
304 fn test_agents_register_then_list() {
305 let mut env = TestEnv::fresh();
306 let db = env.db_path.clone();
307 let reg = AgentsArgs {
308 action: Some(AgentsAction::Register {
309 agent_id: "agent-2".to_string(),
310 agent_type: "system".to_string(),
311 capabilities: String::new(),
312 }),
313 };
314 {
315 let mut out = env.output();
316 run_agents(&db, reg, false, &mut out).unwrap();
317 }
318 env.stdout.clear();
319 env.stderr.clear();
320 let list = AgentsArgs {
321 action: Some(AgentsAction::List),
322 };
323 {
324 let mut out = env.output();
325 run_agents(&db, list, false, &mut out).unwrap();
326 }
327 let s = env.stdout_str();
328 assert!(s.contains("agent-2"));
329 assert!(s.contains("type=system"));
330 }
331
332 #[test]
333 fn test_agents_register_invalid_agent_id() {
334 let mut env = TestEnv::fresh();
335 let db = env.db_path.clone();
336 let args = AgentsArgs {
337 action: Some(AgentsAction::Register {
338 agent_id: String::new(), agent_type: "human".to_string(),
340 capabilities: String::new(),
341 }),
342 };
343 let mut out = env.output();
344 let res = run_agents(&db, args, false, &mut out);
345 assert!(res.is_err());
346 }
347
348 #[test]
349 fn test_agents_default_action_is_list() {
350 let mut env = TestEnv::fresh();
351 let db = env.db_path.clone();
352 let args = AgentsArgs { action: None };
353 {
354 let mut out = env.output();
355 run_agents(&db, args, false, &mut out).unwrap();
356 }
357 assert!(env.stdout_str().contains("no registered agents"));
358 }
359
360 #[test]
361 fn test_pending_list_empty() {
362 let mut env = TestEnv::fresh();
363 let db = env.db_path.clone();
364 let args = PendingArgs {
365 action: PendingAction::List {
366 status: None,
367 limit: 100,
368 },
369 };
370 {
371 let mut out = env.output();
372 run_pending(&db, args, false, Some("test-agent"), &mut out).unwrap();
373 }
374 assert!(env.stdout_str().contains("no pending actions"));
375 }
376
377 #[test]
378 fn test_pending_list_empty_json() {
379 let mut env = TestEnv::fresh();
380 let db = env.db_path.clone();
381 let args = PendingArgs {
382 action: PendingAction::List {
383 status: Some("pending".to_string()),
384 limit: 100,
385 },
386 };
387 {
388 let mut out = env.output();
389 run_pending(&db, args, true, Some("test-agent"), &mut out).unwrap();
390 }
391 let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
392 assert_eq!(v["count"].as_u64().unwrap(), 0);
393 }
394}