Skip to main content

jacs_cli/
main.rs

1use clap::{Arg, ArgAction, Command, crate_name, value_parser};
2
3mod agent_loader;
4mod password_bootstrap;
5
6use agent_loader::{load_agent, load_agent_with_cli_dns_policy};
7use jacs::agent::Agent;
8use jacs::agent::boilerplate::BoilerPlate;
9use jacs::agent::document::DocumentTraits;
10use jacs::cli_utils::create::{
11    handle_agent_create, handle_agent_create_auto, handle_config_create,
12};
13use jacs::cli_utils::default_set_file_list;
14use jacs::cli_utils::document::{
15    check_agreement, create_agreement, create_documents, extract_documents, sign_documents,
16    update_documents, verify_documents,
17};
18use jacs::create_task; // re-enabled: may be used by a2a later
19use jacs::dns::bootstrap as dns_bootstrap;
20use jacs::shutdown::{ShutdownGuard, install_signal_handler};
21use password_bootstrap::{
22    ensure_cli_private_key_password, quickstart_password_bootstrap_help,
23    wrap_quickstart_error_with_password_help,
24};
25
26use rpassword::read_password;
27use std::env;
28use std::error::Error;
29use std::process;
30
31// install/download functions removed — MCP is now built into the CLI
32
33/// Build the Clap `Command` tree for the JACS CLI.
34///
35/// Exposed as a public function so that snapshot tests can walk
36/// the command tree programmatically without hardcoded lists.
37pub fn build_cli() -> Command {
38    let cmd = Command::new(crate_name!())
39        .version(env!("CARGO_PKG_VERSION"))
40        .about(env!("CARGO_PKG_DESCRIPTION"))
41        .subcommand(
42            Command::new("version")
43                .about("Prints version and build information")
44        )
45        .subcommand(
46            Command::new("config")
47                .about(" work with JACS configuration")
48                .subcommand(
49                    Command::new("create")
50                        .about(" create a config file")
51                )
52                .subcommand(
53                    Command::new("read")
54                    .about("read configuration and display to screen. This includes both the config file and the env variables.")
55                ),
56        )
57        .subcommand(
58            Command::new("agent")
59                .about(" work with a JACS agent")
60                .subcommand(
61                    Command::new("dns")
62                        .about("emit DNS TXT commands for publishing agent fingerprint")
63                        .arg(
64                            Arg::new("agent-file")
65                                .short('a')
66                                .long("agent-file")
67                                .value_parser(value_parser!(String))
68                                .help("Path to agent JSON (optional; defaults via config)"),
69                        )
70                        .arg(
71                            Arg::new("no-dns")
72                                .long("no-dns")
73                                .help("Disable DNS validation; rely on embedded fingerprint")
74                                .action(ArgAction::SetTrue),
75                        )
76                        .arg(
77                            Arg::new("require-dns")
78                                .long("require-dns")
79                                .help("Require DNS validation; if domain missing, fail. Not strict (no DNSSEC required).")
80                                .action(ArgAction::SetTrue),
81                        )
82                        .arg(
83                            Arg::new("require-strict-dns")
84                                .long("require-strict-dns")
85                                .help("Require strict DNSSEC validation; if domain missing, fail.")
86                                .action(ArgAction::SetTrue),
87                        )
88                        .arg(
89                            Arg::new("ignore-dns")
90                                .long("ignore-dns")
91                                .help("Ignore DNS validation entirely.")
92                                .action(ArgAction::SetTrue),
93                        )
94                        .arg(Arg::new("domain").long("domain").value_parser(value_parser!(String)))
95                        .arg(Arg::new("agent-id").long("agent-id").value_parser(value_parser!(String)))
96                        .arg(Arg::new("ttl").long("ttl").value_parser(value_parser!(u32)).default_value("3600"))
97                        .arg(Arg::new("encoding").long("encoding").value_parser(["base64","hex"]).default_value("base64"))
98                        .arg(Arg::new("provider").long("provider").value_parser(["plain","aws","azure","cloudflare"]).default_value("plain"))
99                )
100                .subcommand(
101                    Command::new("create")
102                        .about(" create an agent")
103                        .arg(
104                            Arg::new("filename")
105                                .short('f')
106                                .help("Name of the json file with agent schema and jacsAgentType")
107                                .value_parser(value_parser!(String)),
108                        )
109                        .arg(
110                            Arg::new("create-keys")
111                                .long("create-keys")
112                                .required(true)
113                                .help("Create keys or not if they already exist. Configure key type in jacs.config.json")
114                                .value_parser(value_parser!(bool)),
115                        ),
116                )
117                .subcommand(
118                    Command::new("verify")
119                    .about(" verify an agent")
120                    .arg(
121                        Arg::new("agent-file")
122                            .short('a')
123                            .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
124                            .value_parser(value_parser!(String)),
125                    )
126                    .arg(
127                        Arg::new("no-dns")
128                            .long("no-dns")
129                            .help("Disable DNS validation; rely on embedded fingerprint")
130                            .action(ArgAction::SetTrue),
131                    )
132                    .arg(
133                        Arg::new("require-dns")
134                            .long("require-dns")
135                            .help("Require DNS validation; if domain missing, fail. Not strict (no DNSSEC required).")
136                            .action(ArgAction::SetTrue),
137                    )
138                    .arg(
139                        Arg::new("require-strict-dns")
140                            .long("require-strict-dns")
141                            .help("Require strict DNSSEC validation; if domain missing, fail.")
142                            .action(ArgAction::SetTrue),
143                    )
144                    .arg(
145                        Arg::new("ignore-dns")
146                            .long("ignore-dns")
147                            .help("Ignore DNS validation entirely.")
148                            .action(ArgAction::SetTrue),
149                    ),
150                )
151                .subcommand(
152                    Command::new("lookup")
153                        .about("Look up another agent's public key and DNS info from their domain")
154                        .arg(
155                            Arg::new("domain")
156                                .required(true)
157                                .help("Domain to look up (e.g., agent.example.com)"),
158                        )
159                        .arg(
160                            Arg::new("no-dns")
161                                .long("no-dns")
162                                .help("Skip DNS TXT record lookup")
163                                .action(ArgAction::SetTrue),
164                        )
165                        .arg(
166                            Arg::new("strict")
167                                .long("strict")
168                                .help("Require DNSSEC validation for DNS lookup")
169                                .action(ArgAction::SetTrue),
170                        ),
171                )
172                .subcommand(
173                    Command::new("rotate-keys")
174                        .about("Rotate the agent's cryptographic keys")
175                        .arg(
176                            Arg::new("algorithm")
177                                .long("algorithm")
178                                .value_parser(["ring-Ed25519", "pq2025"])
179                                .help("Signing algorithm for the new keys (defaults to current)"),
180                        )
181                        .arg(
182                            Arg::new("config")
183                                .long("config")
184                                .value_parser(value_parser!(String))
185                                .help("Path to jacs.config.json (default: ./jacs.config.json)"),
186                        ),
187                )
188                .subcommand(
189                    Command::new("keys-list")
190                        .about("List active and archived key files")
191                        .arg(
192                            Arg::new("config")
193                                .long("config")
194                                .value_parser(value_parser!(String))
195                                .help("Path to jacs.config.json (default: ./jacs.config.json)"),
196                        ),
197                )
198                .subcommand(
199                    Command::new("repair")
200                        .about("Repair config after an interrupted key rotation")
201                        .arg(
202                            Arg::new("config")
203                                .long("config")
204                                .value_parser(value_parser!(String))
205                                .help("Path to jacs.config.json (default: ./jacs.config.json)"),
206                        ),
207                ),
208        )
209
210        .subcommand(
211            Command::new("task")
212            .about(" work with a JACS  Agent task")
213            .subcommand(
214                Command::new("create")
215                    .about(" create a new JACS Task file, either by embedding or parsing a document")
216                    .arg(
217                        Arg::new("agent-file")
218                            .short('a')
219                            .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
220                            .value_parser(value_parser!(String)),
221                    )
222                    .arg(
223                        Arg::new("filename")
224                            .short('f')
225                            .help("Path to input file. Must be JSON")
226                            .value_parser(value_parser!(String)),
227                    )
228                    .arg(
229                        Arg::new("name")
230                            .short('n')
231                            .required(true)
232                            .help("name of task")
233                            .value_parser(value_parser!(String)),
234                    )
235                    .arg(
236                        Arg::new("description")
237                            .short('d')
238                            .required(true)
239                            .help("description of task")
240                            .value_parser(value_parser!(String)),
241                    )
242                )
243                .subcommand(
244                    Command::new("update")
245                        .about("update an existing task document")
246                        .arg(
247                            Arg::new("filename")
248                                .short('f')
249                                .required(true)
250                                .help("Path to the updated task JSON file")
251                                .value_parser(value_parser!(String)),
252                        )
253                        .arg(
254                            Arg::new("task-key")
255                                .short('k')
256                                .required(true)
257                                .help("Task document key (id:version)")
258                                .value_parser(value_parser!(String)),
259                        )
260                )
261            )
262
263        .subcommand(
264            Command::new("document")
265                .about(" work with a general JACS document")
266                .subcommand(
267                    Command::new("create")
268                        .about(" create a new JACS file, either by embedding or parsing a document")
269                        .arg(
270                            Arg::new("agent-file")
271                                .short('a')
272                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
273                                .value_parser(value_parser!(String)),
274                        )
275                        .arg(
276                            Arg::new("filename")
277                                .short('f')
278                                .help("Path to input file. Must be JSON")
279                                .value_parser(value_parser!(String)),
280                        )
281                        .arg(
282                            Arg::new("output")
283                                .short('o')
284                                .help("Output filename. ")
285                                .value_parser(value_parser!(String)),
286                        )
287                        .arg(
288                            Arg::new("directory")
289                                .short('d')
290                                .help("Path to directory of files. Files should end with .json")
291                                .value_parser(value_parser!(String)),
292                        )
293                        .arg(
294                            Arg::new("verbose")
295                                .short('v')
296                                .long("verbose")
297                                .action(ArgAction::SetTrue),
298                        )
299                        .arg(
300                            Arg::new("no-save")
301                                .long("no-save")
302                                .short('n')
303                                .help("Instead of saving files, print to stdout")
304                                .action(ArgAction::SetTrue),
305                        )
306                        .arg(
307                            Arg::new("schema")
308                                .short('s')
309                                .help("Path to JSON schema file to use to create")
310                                .long("schema")
311                                .value_parser(value_parser!(String)),
312                        )
313                        .arg(
314                            Arg::new("attach")
315                                .help("Path to file or directory for file attachments")
316                                .long("attach")
317                                .value_parser(value_parser!(String)),
318                        )
319                        .arg(
320                            Arg::new("embed")
321                                .short('e')
322                                .help("Embed documents or keep the documents external")
323                                .long("embed")
324                                .value_parser(value_parser!(bool)),
325                        ),
326                )
327                .subcommand(
328                    Command::new("update")
329                        .about("create a new version of document. requires both the original JACS file and the modified jacs metadata")
330                        .arg(
331                            Arg::new("agent-file")
332                                .short('a')
333                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
334                                .value_parser(value_parser!(String)),
335                        )
336                        .arg(
337                            Arg::new("new")
338                                .short('n')
339                                .required(true)
340                                .help("Path to new version of document.")
341                                .value_parser(value_parser!(String)),
342                        )
343                        .arg(
344                            Arg::new("filename")
345                                .short('f')
346                                .required(true)
347                                .help("Path to original document.")
348                                .value_parser(value_parser!(String)),
349                        )
350                        .arg(
351                            Arg::new("output")
352                                .short('o')
353                                .help("Output filename. Filenames will always end with \"json\"")
354                                .value_parser(value_parser!(String)),
355                        )
356                        .arg(
357                            Arg::new("verbose")
358                                .short('v')
359                                .long("verbose")
360                                .action(ArgAction::SetTrue),
361                        )
362                        .arg(
363                            Arg::new("no-save")
364                                .long("no-save")
365                                .short('n')
366                                .help("Instead of saving files, print to stdout")
367                                .action(ArgAction::SetTrue),
368                        )
369                        .arg(
370                            Arg::new("schema")
371                                .short('s')
372                                .help("Path to JSON schema file to use to create")
373                                .long("schema")
374                                .value_parser(value_parser!(String)),
375                        )
376                        .arg(
377                            Arg::new("attach")
378                                .help("Path to file or directory for file attachments")
379                                .long("attach")
380                                .value_parser(value_parser!(String)),
381                        )
382                        .arg(
383                            Arg::new("embed")
384                                .short('e')
385                                .help("Embed documents or keep the documents external")
386                                .long("embed")
387                                .value_parser(value_parser!(bool)),
388                        )
389                        ,
390                )
391                .subcommand(
392                    Command::new("check-agreement")
393                        .about("given a document, provide alist of agents that should sign document")
394                        .arg(
395                            Arg::new("agent-file")
396                                .short('a')
397                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
398                                .value_parser(value_parser!(String)),
399                        )
400                        .arg(
401                            Arg::new("filename")
402                                .short('f')
403                                .required(true)
404                                .help("Path to original document.")
405                                .value_parser(value_parser!(String)),
406                        )
407                        .arg(
408                            Arg::new("directory")
409                                .short('d')
410                                .help("Path to directory of files. Files should end with .json")
411                                .value_parser(value_parser!(String)),
412                        )
413                        .arg(
414                            Arg::new("schema")
415                                .short('s')
416                                .help("Path to JSON schema file to use to create")
417                                .long("schema")
418                                .value_parser(value_parser!(String)),
419                        )
420
421                )
422                .subcommand(
423                    Command::new("create-agreement")
424                        .about("given a document, provide alist of agents that should sign document")
425                        .arg(
426                            Arg::new("agent-file")
427                                .short('a')
428                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
429                                .value_parser(value_parser!(String)),
430                        )
431                        .arg(
432                            Arg::new("filename")
433                                .short('f')
434                                .required(true)
435                                .help("Path to original document.")
436                                .value_parser(value_parser!(String)),
437                        )
438                        .arg(
439                            Arg::new("directory")
440                                .short('d')
441                                .help("Path to directory of files. Files should end with .json")
442                                .value_parser(value_parser!(String)),
443                        )
444                        .arg(
445                                Arg::new("agentids")
446                                .short('i')
447                                .long("agentids")
448                                .value_name("VALUES")
449                                .help("Comma-separated list of agent ids")
450                                .value_delimiter(',')
451                                .required(true)
452                                .action(clap::ArgAction::Set),
453                            )
454                        .arg(
455                            Arg::new("output")
456                                .short('o')
457                                .help("Output filename. Filenames will always end with \"json\"")
458                                .value_parser(value_parser!(String)),
459                        )
460                        .arg(
461                            Arg::new("verbose")
462                                .short('v')
463                                .long("verbose")
464                                .action(ArgAction::SetTrue),
465                        )
466                        .arg(
467                            Arg::new("no-save")
468                                .long("no-save")
469                                .short('n')
470                                .help("Instead of saving files, print to stdout")
471                                .action(ArgAction::SetTrue),
472                        )
473                        .arg(
474                            Arg::new("schema")
475                                .short('s')
476                                .help("Path to JSON schema file to use to create")
477                                .long("schema")
478                                .value_parser(value_parser!(String)),
479                        )
480
481                ).subcommand(
482                    Command::new("sign-agreement")
483                        .about("given a document, sign the agreement section")
484                        .arg(
485                            Arg::new("agent-file")
486                                .short('a')
487                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
488                                .value_parser(value_parser!(String)),
489                        )
490                        .arg(
491                            Arg::new("filename")
492                                .short('f')
493                                .required(true)
494                                .help("Path to original document.")
495                                .value_parser(value_parser!(String)),
496                        )
497                        .arg(
498                            Arg::new("directory")
499                                .short('d')
500                                .help("Path to directory of files. Files should end with .json")
501                                .value_parser(value_parser!(String)),
502                        )
503                        .arg(
504                            Arg::new("output")
505                                .short('o')
506                                .help("Output filename. Filenames will always end with \"json\"")
507                                .value_parser(value_parser!(String)),
508                        )
509                        .arg(
510                            Arg::new("verbose")
511                                .short('v')
512                                .long("verbose")
513                                .action(ArgAction::SetTrue),
514                        )
515                        .arg(
516                            Arg::new("no-save")
517                                .long("no-save")
518                                .short('n')
519                                .help("Instead of saving files, print to stdout")
520                                .action(ArgAction::SetTrue),
521                        )
522                        .arg(
523                            Arg::new("schema")
524                                .short('s')
525                                .help("Path to JSON schema file to use to create")
526                                .long("schema")
527                                .value_parser(value_parser!(String)),
528                        )
529
530                )
531                .subcommand(
532                    Command::new("verify")
533                        .about(" verify a documents hash, siginatures, and schema")
534                        .arg(
535                            Arg::new("agent-file")
536                                .short('a')
537                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
538                                .value_parser(value_parser!(String)),
539                        )
540                        .arg(
541                            Arg::new("filename")
542                                .short('f')
543                                .help("Path to input file. Must be JSON")
544                                .value_parser(value_parser!(String)),
545                        )
546                        .arg(
547                            Arg::new("directory")
548                                .short('d')
549                                .help("Path to directory of files. Files should end with .json")
550                                .value_parser(value_parser!(String)),
551                        )
552                        .arg(
553                            Arg::new("verbose")
554                                .short('v')
555                                .long("verbose")
556                                .action(ArgAction::SetTrue),
557                        )
558                        .arg(
559                            Arg::new("schema")
560                                .short('s')
561                                .help("Path to JSON schema file to use to validate")
562                                .long("schema")
563                                .value_parser(value_parser!(String)),
564                        ),
565                )
566                .subcommand(
567                    Command::new("extract")
568                        .about(" given  documents, extract embedded contents if any")
569                        .arg(
570                            Arg::new("agent-file")
571                                .short('a')
572                                .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
573                                .value_parser(value_parser!(String)),
574                        )
575                        .arg(
576                            Arg::new("filename")
577                                .short('f')
578                                .help("Path to input file. Must be JSON")
579                                .value_parser(value_parser!(String)),
580                        )
581                        .arg(
582                            Arg::new("directory")
583                                .short('d')
584                                .help("Path to directory of files. Files should end with .json")
585                                .value_parser(value_parser!(String)),
586                        )
587                        .arg(
588                            Arg::new("verbose")
589                                .short('v')
590                                .long("verbose")
591                                .action(ArgAction::SetTrue),
592                        )
593                        .arg(
594                            Arg::new("schema")
595                                .short('s')
596                                .help("Path to JSON schema file to use to validate")
597                                .long("schema")
598                                .value_parser(value_parser!(String)),
599                        ),
600                )
601        )
602        .subcommand(
603            Command::new("key")
604                .about("Work with JACS cryptographic keys")
605                .subcommand(
606                    Command::new("reencrypt")
607                        .about("Re-encrypt the private key with a new password")
608                )
609        )
610        .subcommand(
611            Command::new("mcp")
612                .about("Start the built-in JACS MCP server (stdio transport)")
613                .arg(
614                    Arg::new("profile")
615                        .long("profile")
616                        .default_value("core")
617                        .help("Tool profile: 'core' (default, core tools) or 'full' (all tools)"),
618                )
619                .subcommand(
620                    Command::new("install")
621                        .about("Deprecated: MCP is now built into the jacs binary")
622                        .hide(true)
623                )
624                .subcommand(
625                    Command::new("run")
626                        .about("Deprecated: use `jacs mcp` directly")
627                        .hide(true)
628                ),
629        )
630        .subcommand(
631            Command::new("a2a")
632                .about("A2A (Agent-to-Agent) trust and discovery commands")
633                .subcommand(
634                    Command::new("assess")
635                        .about("Assess trust level of a remote A2A Agent Card")
636                        .arg(
637                            Arg::new("source")
638                                .required(true)
639                                .help("Path to Agent Card JSON file or URL"),
640                        )
641                        .arg(
642                            Arg::new("policy")
643                                .long("policy")
644                                .short('p')
645                                .value_parser(["open", "verified", "strict"])
646                                .default_value("verified")
647                                .help("Trust policy to apply (default: verified)"),
648                        )
649                        .arg(
650                            Arg::new("json")
651                                .long("json")
652                                .action(ArgAction::SetTrue)
653                                .help("Output result as JSON"),
654                        ),
655                )
656                .subcommand(
657                    Command::new("trust")
658                        .about("Add a remote A2A agent to the local trust store")
659                        .arg(
660                            Arg::new("source")
661                                .required(true)
662                                .help("Path to Agent Card JSON file or URL"),
663                        ),
664                )
665                .subcommand(
666                    Command::new("discover")
667                        .about("Discover a remote A2A agent via its well-known Agent Card")
668                        .arg(
669                            Arg::new("url")
670                                .required(true)
671                                .help("Base URL of the agent (e.g. https://agent.example.com)"),
672                        )
673                        .arg(
674                            Arg::new("json")
675                                .long("json")
676                                .action(ArgAction::SetTrue)
677                                .help("Output the full Agent Card as JSON"),
678                        )
679                        .arg(
680                            Arg::new("policy")
681                                .long("policy")
682                                .short('p')
683                                .value_parser(["open", "verified", "strict"])
684                                .default_value("verified")
685                                .help("Trust policy to apply against the discovered card"),
686                        ),
687                )
688                .subcommand(
689                    Command::new("serve")
690                        .about("Serve this agent's .well-known endpoints for A2A discovery")
691                        .arg(
692                            Arg::new("port")
693                                .long("port")
694                                .value_parser(value_parser!(u16))
695                                .default_value("8080")
696                                .help("Port to listen on (default: 8080)"),
697                        )
698                        .arg(
699                            Arg::new("host")
700                                .long("host")
701                                .default_value("127.0.0.1")
702                                .help("Host to bind to (default: 127.0.0.1)"),
703                        ),
704                )
705                .subcommand(
706                    Command::new("quickstart")
707                        .about("Create/load an agent and start serving A2A endpoints (password required)")
708                        .after_help(quickstart_password_bootstrap_help())
709                        .arg(
710                            Arg::new("name")
711                                .long("name")
712                                .value_parser(value_parser!(String))
713                                .required(true)
714                                .help("Agent name used for first-time quickstart creation"),
715                        )
716                        .arg(
717                            Arg::new("domain")
718                                .long("domain")
719                                .value_parser(value_parser!(String))
720                                .required(true)
721                                .help("Agent domain used for DNS/public-key verification workflows"),
722                        )
723                        .arg(
724                            Arg::new("description")
725                                .long("description")
726                                .value_parser(value_parser!(String))
727                                .help("Optional human-readable agent description"),
728                        )
729                        .arg(
730                            Arg::new("port")
731                                .long("port")
732                                .value_parser(value_parser!(u16))
733                                .default_value("8080")
734                                .help("Port to listen on (default: 8080)"),
735                        )
736                        .arg(
737                            Arg::new("host")
738                                .long("host")
739                                .default_value("127.0.0.1")
740                                .help("Host to bind to (default: 127.0.0.1)"),
741                        )
742                        .arg(
743                            Arg::new("algorithm")
744                                .long("algorithm")
745                                .short('a')
746                                .value_parser(["pq2025", "ring-Ed25519"])
747                                .help("Signing algorithm (default: pq2025)"),
748                        ),
749                ),
750        )
751        .subcommand(
752            Command::new("quickstart")
753                .about("Create or load a persistent agent for instant sign/verify (password required)")
754                .after_help(quickstart_password_bootstrap_help())
755                .arg(
756                    Arg::new("name")
757                        .long("name")
758                        .value_parser(value_parser!(String))
759                        .required(true)
760                        .help("Agent name used for first-time quickstart creation"),
761                )
762                .arg(
763                    Arg::new("domain")
764                        .long("domain")
765                        .value_parser(value_parser!(String))
766                        .required(true)
767                        .help("Agent domain used for DNS/public-key verification workflows"),
768                )
769                .arg(
770                    Arg::new("description")
771                        .long("description")
772                        .value_parser(value_parser!(String))
773                        .help("Optional human-readable agent description"),
774                )
775                .arg(
776                    Arg::new("algorithm")
777                        .long("algorithm")
778                        .short('a')
779                        .value_parser(["ed25519", "pq2025"])
780                        .default_value("pq2025")
781                        .help("Signing algorithm (default: pq2025)"),
782                )
783                .arg(
784                    Arg::new("sign")
785                        .long("sign")
786                        .help("Sign JSON from stdin and print signed document to stdout")
787                        .action(ArgAction::SetTrue),
788                )
789                .arg(
790                    Arg::new("file")
791                        .short('f')
792                        .long("file")
793                        .value_parser(value_parser!(String))
794                        .help("Sign a JSON file instead of reading from stdin (used with --sign)"),
795                )
796        )
797        .subcommand(
798            Command::new("init")
799                .about("Initialize JACS by creating both config and agent (with keys)")
800                .arg(
801                    Arg::new("yes")
802                        .long("yes")
803                        .short('y')
804                        .action(ArgAction::SetTrue)
805                        .help("Automatically set the new agent ID in jacs.config.json without prompting"),
806                )
807        )
808        .subcommand(
809            Command::new("attest")
810                .about("Create and verify attestation documents")
811                .subcommand(
812                    Command::new("create")
813                        .about("Create a signed attestation")
814                        .arg(
815                            Arg::new("subject-type")
816                                .long("subject-type")
817                                .value_parser(["agent", "artifact", "workflow", "identity"])
818                                .help("Type of subject being attested"),
819                        )
820                        .arg(
821                            Arg::new("subject-id")
822                                .long("subject-id")
823                                .value_parser(value_parser!(String))
824                                .help("Identifier of the subject"),
825                        )
826                        .arg(
827                            Arg::new("subject-digest")
828                                .long("subject-digest")
829                                .value_parser(value_parser!(String))
830                                .help("SHA-256 digest of the subject"),
831                        )
832                        .arg(
833                            Arg::new("claims")
834                                .long("claims")
835                                .value_parser(value_parser!(String))
836                                .required(true)
837                                .help("JSON array of claims, e.g. '[{\"name\":\"reviewed\",\"value\":true}]'"),
838                        )
839                        .arg(
840                            Arg::new("evidence")
841                                .long("evidence")
842                                .value_parser(value_parser!(String))
843                                .help("JSON array of evidence references"),
844                        )
845                        .arg(
846                            Arg::new("from-document")
847                                .long("from-document")
848                                .value_parser(value_parser!(String))
849                                .help("Lift attestation from an existing signed document file"),
850                        )
851                        .arg(
852                            Arg::new("output")
853                                .short('o')
854                                .long("output")
855                                .value_parser(value_parser!(String))
856                                .help("Write attestation to file instead of stdout"),
857                        ),
858                )
859                .subcommand(
860                    Command::new("verify")
861                        .about("Verify an attestation document")
862                        .arg(
863                            Arg::new("file")
864                                .help("Path to the attestation JSON file")
865                                .required(true)
866                                .value_parser(value_parser!(String)),
867                        )
868                        .arg(
869                            Arg::new("full")
870                                .long("full")
871                                .action(ArgAction::SetTrue)
872                                .help("Use full verification (evidence + derivation chain)"),
873                        )
874                        .arg(
875                            Arg::new("json")
876                                .long("json")
877                                .action(ArgAction::SetTrue)
878                                .help("Output result as JSON"),
879                        )
880                        .arg(
881                            Arg::new("key-dir")
882                                .long("key-dir")
883                                .value_parser(value_parser!(String))
884                                .help("Directory containing public keys for verification"),
885                        )
886                        .arg(
887                            Arg::new("max-depth")
888                                .long("max-depth")
889                                .value_parser(value_parser!(u32))
890                                .help("Maximum derivation chain depth"),
891                        ),
892                )
893                .subcommand(
894                    Command::new("export-dsse")
895                        .about("Export an attestation as a DSSE envelope for in-toto/SLSA")
896                        .arg(
897                            Arg::new("file")
898                                .help("Path to the signed attestation JSON file")
899                                .required(true)
900                                .value_parser(value_parser!(String)),
901                        )
902                        .arg(
903                            Arg::new("output")
904                                .short('o')
905                                .long("output")
906                                .value_parser(value_parser!(String))
907                                .help("Write DSSE envelope to file instead of stdout"),
908                        ),
909                )
910                .subcommand_required(true)
911                .arg_required_else_help(true),
912        )
913        .subcommand(
914            Command::new("verify")
915                .about("Verify a signed JACS document (no agent required)")
916                .arg(
917                    Arg::new("file")
918                        .help("Path to the signed JACS JSON file")
919                        .required_unless_present("remote")
920                        .value_parser(value_parser!(String)),
921                )
922                .arg(
923                    Arg::new("remote")
924                        .long("remote")
925                        .value_parser(value_parser!(String))
926                        .help("Fetch document from URL before verifying"),
927                )
928                .arg(
929                    Arg::new("json")
930                        .long("json")
931                        .action(ArgAction::SetTrue)
932                        .help("Output result as JSON"),
933                )
934                .arg(
935                    Arg::new("key-dir")
936                        .long("key-dir")
937                        .value_parser(value_parser!(String))
938                        .help("Directory containing public keys for verification"),
939                )
940        );
941
942    // OS keychain subcommand (only when keychain feature is enabled)
943    #[cfg(feature = "keychain")]
944    let cmd = cmd.subcommand(
945        Command::new("keychain")
946            .about("Manage private key passwords in the OS keychain (per-agent)")
947            .subcommand(
948                Command::new("set")
949                    .about("Store a password in the OS keychain for an agent")
950                    .arg(
951                        Arg::new("agent-id")
952                            .long("agent-id")
953                            .help("Agent ID to associate the password with")
954                            .value_name("AGENT_ID")
955                            .required(true),
956                    )
957                    .arg(
958                        Arg::new("password")
959                            .long("password")
960                            .help("Password to store (if omitted, prompts interactively)")
961                            .value_name("PASSWORD"),
962                    ),
963            )
964            .subcommand(
965                Command::new("get")
966                    .about("Retrieve the stored password for an agent (prints to stdout)")
967                    .arg(
968                        Arg::new("agent-id")
969                            .long("agent-id")
970                            .help("Agent ID to look up")
971                            .value_name("AGENT_ID")
972                            .required(true),
973                    ),
974            )
975            .subcommand(
976                Command::new("delete")
977                    .about("Remove the stored password for an agent from the OS keychain")
978                    .arg(
979                        Arg::new("agent-id")
980                            .long("agent-id")
981                            .help("Agent ID whose password to delete")
982                            .value_name("AGENT_ID")
983                            .required(true),
984                    ),
985            )
986            .subcommand(
987                Command::new("status")
988                    .about("Check if a password is stored for an agent in the OS keychain")
989                    .arg(
990                        Arg::new("agent-id")
991                            .long("agent-id")
992                            .help("Agent ID to check")
993                            .value_name("AGENT_ID")
994                            .required(true),
995                    ),
996            )
997            .arg_required_else_help(true),
998    );
999
1000    let cmd = cmd.subcommand(
1001        Command::new("convert")
1002            .about(
1003                "Convert JACS documents between JSON, YAML, and HTML formats (no agent required)",
1004            )
1005            .arg(
1006                Arg::new("to")
1007                    .long("to")
1008                    .required(true)
1009                    .value_parser(["json", "yaml", "html"])
1010                    .help("Target format: json, yaml, or html"),
1011            )
1012            .arg(
1013                Arg::new("from")
1014                    .long("from")
1015                    .value_parser(["json", "yaml", "html"])
1016                    .help("Source format (auto-detected from extension if omitted)"),
1017            )
1018            .arg(
1019                Arg::new("file")
1020                    .short('f')
1021                    .long("file")
1022                    .required(true)
1023                    .value_parser(value_parser!(String))
1024                    .help("Input file path (use '-' for stdin)"),
1025            )
1026            .arg(
1027                Arg::new("output")
1028                    .short('o')
1029                    .long("output")
1030                    .value_parser(value_parser!(String))
1031                    .help("Output file path (defaults to stdout)"),
1032            ),
1033    );
1034
1035    cmd
1036}
1037
1038pub fn main() -> Result<(), Box<dyn Error>> {
1039    // Install signal handler for graceful shutdown (Ctrl+C, SIGTERM)
1040    install_signal_handler();
1041
1042    // Create shutdown guard to ensure cleanup on exit (including early returns)
1043    let _shutdown_guard = ShutdownGuard::new();
1044    let matches = build_cli().arg_required_else_help(true).get_matches();
1045
1046    match matches.subcommand() {
1047        Some(("version", _sub_matches)) => {
1048            println!("{}", env!("CARGO_PKG_DESCRIPTION"));
1049            println!(
1050                "{} version: {}",
1051                env!("CARGO_PKG_NAME"),
1052                env!("CARGO_PKG_VERSION")
1053            );
1054            return Ok(());
1055        }
1056        Some(("config", config_matches)) => match config_matches.subcommand() {
1057            Some(("create", _create_matches)) => {
1058                // Call the refactored handler function
1059                handle_config_create()?;
1060            }
1061            Some(("read", _read_matches)) => {
1062                let config_path = "./jacs.config.json";
1063                match jacs::config::Config::from_file(config_path) {
1064                    Ok(mut config) => {
1065                        config.apply_env_overrides();
1066                        println!("{}", config);
1067                    }
1068                    Err(e) => {
1069                        eprintln!("Could not load config from '{}': {}", config_path, e);
1070                        process::exit(1);
1071                    }
1072                }
1073            }
1074            _ => println!("please enter subcommand see jacs config --help"),
1075        },
1076        Some(("agent", agent_matches)) => match agent_matches.subcommand() {
1077            Some(("dns", sub_m)) => {
1078                let domain = sub_m.get_one::<String>("domain").cloned();
1079                let agent_id_arg = sub_m.get_one::<String>("agent-id").cloned();
1080                let ttl = *sub_m.get_one::<u32>("ttl").unwrap();
1081                let enc = sub_m
1082                    .get_one::<String>("encoding")
1083                    .map(|s| s.as_str())
1084                    .unwrap_or("base64");
1085                let provider = sub_m
1086                    .get_one::<String>("provider")
1087                    .map(|s| s.as_str())
1088                    .unwrap_or("plain");
1089
1090                // Load agent from optional path, supporting non-strict DNS for propagation
1091                let _agent_file = sub_m.get_one::<String>("agent-file").cloned();
1092                let non_strict = *sub_m.get_one::<bool>("no-dns").unwrap_or(&false);
1093                let ignore_dns = *sub_m.get_one::<bool>("ignore-dns").unwrap_or(&false);
1094                let require_strict = *sub_m
1095                    .get_one::<bool>("require-strict-dns")
1096                    .unwrap_or(&false);
1097                let require_dns = *sub_m.get_one::<bool>("require-dns").unwrap_or(&false);
1098                let agent: Agent = load_agent_with_cli_dns_policy(
1099                    ignore_dns,
1100                    require_strict,
1101                    require_dns,
1102                    non_strict,
1103                )
1104                .expect("Failed to load agent from config");
1105                let agent_id = agent_id_arg.unwrap_or_else(|| agent.get_id().unwrap_or_default());
1106                let pk = agent.get_public_key().expect("public key");
1107                let digest = match enc {
1108                    "hex" => dns_bootstrap::pubkey_digest_hex(&pk),
1109                    _ => dns_bootstrap::pubkey_digest_b64(&pk),
1110                };
1111                let domain_final = domain
1112                    .or_else(|| {
1113                        agent
1114                            .config
1115                            .as_ref()
1116                            .and_then(|c| c.jacs_agent_domain().clone())
1117                    })
1118                    .expect("domain required via --domain or jacs_agent_domain in config");
1119
1120                let rr = dns_bootstrap::build_dns_record(
1121                    &domain_final,
1122                    ttl,
1123                    &agent_id,
1124                    &digest,
1125                    if enc == "hex" {
1126                        dns_bootstrap::DigestEncoding::Hex
1127                    } else {
1128                        dns_bootstrap::DigestEncoding::Base64
1129                    },
1130                );
1131
1132                println!("Plain/BIND:\n{}", dns_bootstrap::emit_plain_bind(&rr));
1133                match provider {
1134                    "aws" => println!(
1135                        "\nRoute53 change-batch JSON:\n{}",
1136                        dns_bootstrap::emit_route53_change_batch(&rr)
1137                    ),
1138                    "azure" => println!(
1139                        "\nAzure CLI:\n{}",
1140                        dns_bootstrap::emit_azure_cli(
1141                            &rr,
1142                            "$RESOURCE_GROUP",
1143                            &domain_final,
1144                            "_v1.agent.jacs"
1145                        )
1146                    ),
1147                    "cloudflare" => println!(
1148                        "\nCloudflare curl:\n{}",
1149                        dns_bootstrap::emit_cloudflare_curl(&rr, "$ZONE_ID")
1150                    ),
1151                    _ => {}
1152                }
1153                println!(
1154                    "\nChecklist: Ensure DNSSEC is enabled for {domain} and DS is published at registrar.",
1155                    domain = domain_final
1156                );
1157            }
1158            Some(("create", create_matches)) => {
1159                // Parse args for the specific agent create command
1160                let filename = create_matches.get_one::<String>("filename");
1161                let create_keys = *create_matches.get_one::<bool>("create-keys").unwrap();
1162
1163                // Call the refactored handler function
1164                handle_agent_create(filename, create_keys)?;
1165            }
1166            Some(("verify", verify_matches)) => {
1167                let _agentfile = verify_matches.get_one::<String>("agent-file");
1168                let non_strict = *verify_matches.get_one::<bool>("no-dns").unwrap_or(&false);
1169                let require_dns = *verify_matches
1170                    .get_one::<bool>("require-dns")
1171                    .unwrap_or(&false);
1172                let require_strict = *verify_matches
1173                    .get_one::<bool>("require-strict-dns")
1174                    .unwrap_or(&false);
1175                let ignore_dns = *verify_matches
1176                    .get_one::<bool>("ignore-dns")
1177                    .unwrap_or(&false);
1178                let mut agent: Agent = load_agent_with_cli_dns_policy(
1179                    ignore_dns,
1180                    require_strict,
1181                    require_dns,
1182                    non_strict,
1183                )
1184                .expect("Failed to load agent from config");
1185                agent
1186                    .verify_self_signature()
1187                    .expect("signature verification");
1188                println!(
1189                    "Agent {} signature verified OK.",
1190                    agent.get_lookup_id().expect("jacsId")
1191                );
1192            }
1193            Some(("lookup", lookup_matches)) => {
1194                let domain = lookup_matches
1195                    .get_one::<String>("domain")
1196                    .expect("domain required");
1197                let skip_dns = *lookup_matches.get_one::<bool>("no-dns").unwrap_or(&false);
1198                let strict_dns = *lookup_matches.get_one::<bool>("strict").unwrap_or(&false);
1199
1200                println!("Agent Lookup: {}\n", domain);
1201
1202                // Fetch public key from well-known endpoint
1203                println!("Public Key (/.well-known/jacs-pubkey.json):");
1204                let url = format!("https://{}/.well-known/jacs-pubkey.json", domain);
1205                let client = reqwest::blocking::Client::builder()
1206                    .timeout(std::time::Duration::from_secs(10))
1207                    .build()
1208                    .expect("HTTP client");
1209                match client.get(&url).send() {
1210                    Ok(response) => {
1211                        if response.status().is_success() {
1212                            match response.json::<serde_json::Value>() {
1213                                Ok(json) => {
1214                                    println!(
1215                                        "  Agent ID: {}",
1216                                        json.get("agentId")
1217                                            .and_then(|v| v.as_str())
1218                                            .unwrap_or("Not specified")
1219                                    );
1220                                    println!(
1221                                        "  Algorithm: {}",
1222                                        json.get("algorithm")
1223                                            .and_then(|v| v.as_str())
1224                                            .unwrap_or("Not specified")
1225                                    );
1226                                    println!(
1227                                        "  Public Key Hash: {}",
1228                                        json.get("publicKeyHash")
1229                                            .and_then(|v| v.as_str())
1230                                            .unwrap_or("Not specified")
1231                                    );
1232                                    if let Some(pk) = json.get("publicKey").and_then(|v| v.as_str())
1233                                    {
1234                                        let preview = if pk.len() > 60 {
1235                                            format!("{}...", &pk[..60])
1236                                        } else {
1237                                            pk.to_string()
1238                                        };
1239                                        println!("  Public Key: {}", preview);
1240                                    }
1241                                }
1242                                Err(e) => println!("  Error parsing response: {}", e),
1243                            }
1244                        } else {
1245                            println!("  HTTP error: {}", response.status());
1246                        }
1247                    }
1248                    Err(e) => println!("  Error fetching: {}", e),
1249                }
1250
1251                println!();
1252
1253                // DNS TXT record lookup
1254                if !skip_dns {
1255                    println!("DNS TXT Record (_v1.agent.jacs.{}):", domain);
1256                    let owner = format!("_v1.agent.jacs.{}", domain.trim_end_matches('.'));
1257                    let lookup_result = if strict_dns {
1258                        dns_bootstrap::resolve_txt_dnssec(&owner)
1259                    } else {
1260                        dns_bootstrap::resolve_txt_insecure(&owner)
1261                    };
1262                    match lookup_result {
1263                        Ok(txt) => {
1264                            // Parse the TXT record
1265                            match dns_bootstrap::parse_agent_txt(&txt) {
1266                                Ok(parsed) => {
1267                                    println!("  Version: {}", parsed.v);
1268                                    println!("  Agent ID: {}", parsed.jacs_agent_id);
1269                                    println!("  Algorithm: {:?}", parsed.alg);
1270                                    println!("  Encoding: {:?}", parsed.enc);
1271                                    println!("  Public Key Hash: {}", parsed.digest);
1272                                }
1273                                Err(e) => println!("  Error parsing TXT: {}", e),
1274                            }
1275                            println!("  Raw TXT: {}", txt);
1276                        }
1277                        Err(e) => {
1278                            println!("  No DNS TXT record found: {}", e);
1279                            if strict_dns {
1280                                println!("  (Strict DNSSEC validation was required)");
1281                            }
1282                        }
1283                    }
1284                } else {
1285                    println!("DNS TXT Record: Skipped (--no-dns)");
1286                }
1287            }
1288            Some(("rotate-keys", sub_m)) => {
1289                use jacs::simple::SimpleAgent;
1290
1291                let config_path = sub_m.get_one::<String>("config").map(|s| s.as_str());
1292                let algorithm = sub_m.get_one::<String>("algorithm").map(|s| s.as_str());
1293
1294                let agent =
1295                    SimpleAgent::load(config_path, None).map_err(|e| -> Box<dyn Error> {
1296                        Box::new(std::io::Error::new(
1297                            std::io::ErrorKind::Other,
1298                            format!("Failed to load agent: {}", e),
1299                        ))
1300                    })?;
1301
1302                let result = jacs::simple::advanced::rotate(&agent, algorithm).map_err(
1303                    |e| -> Box<dyn Error> {
1304                        Box::new(std::io::Error::new(
1305                            std::io::ErrorKind::Other,
1306                            format!("Key rotation failed: {}", e),
1307                        ))
1308                    },
1309                )?;
1310
1311                println!("Key rotation successful.");
1312                println!("  Agent ID:          {}", result.jacs_id);
1313                println!("  Old version:       {}", result.old_version);
1314                println!("  New version:       {}", result.new_version);
1315                println!("  New key hash:      {}", result.new_public_key_hash);
1316                if result.transition_proof.is_some() {
1317                    println!("  Transition proof:  present");
1318                }
1319            }
1320            Some(("keys-list", sub_m)) => {
1321                let config_path = sub_m.get_one::<String>("config");
1322                let config_p = config_path
1323                    .map(|s| s.as_str())
1324                    .unwrap_or("./jacs.config.json");
1325
1326                let config =
1327                    jacs::config::Config::from_file(config_p).map_err(|e| -> Box<dyn Error> {
1328                        Box::new(std::io::Error::new(
1329                            std::io::ErrorKind::Other,
1330                            format!("Failed to load config: {}", e),
1331                        ))
1332                    })?;
1333
1334                let key_dir = config
1335                    .jacs_key_directory()
1336                    .as_deref()
1337                    .unwrap_or("./jacs_keys");
1338                let algo = config
1339                    .jacs_agent_key_algorithm()
1340                    .as_deref()
1341                    .unwrap_or("unknown");
1342                let pub_name = config
1343                    .jacs_agent_public_key_filename()
1344                    .as_deref()
1345                    .unwrap_or("jacs.public.pem");
1346
1347                // Show active key
1348                let active_path = std::path::Path::new(key_dir).join(pub_name);
1349                if active_path.exists() {
1350                    let meta = std::fs::metadata(&active_path).ok();
1351                    let modified = meta
1352                        .and_then(|m| m.modified().ok())
1353                        .map(|t| {
1354                            let duration =
1355                                t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
1356                            format!("{}s since epoch", duration.as_secs())
1357                        })
1358                        .unwrap_or_else(|| "unknown".to_string());
1359                    println!(
1360                        "Active:   {} (algorithm: {}, modified: {})",
1361                        active_path.display(),
1362                        algo,
1363                        modified
1364                    );
1365                } else {
1366                    println!(
1367                        "Active:   (no public key found at {})",
1368                        active_path.display()
1369                    );
1370                }
1371
1372                // Scan for archived keys
1373                let mut archived: Vec<(String, String)> = Vec::new();
1374                if let Ok(entries) = std::fs::read_dir(key_dir) {
1375                    for entry in entries.flatten() {
1376                        let name = entry.file_name().to_string_lossy().to_string();
1377                        // Archived keys look like: jacs.public.{uuid}.pem
1378                        if name.ends_with(".pem")
1379                            && name.starts_with("jacs.public.")
1380                            && name != pub_name
1381                        {
1382                            let modified = entry
1383                                .metadata()
1384                                .ok()
1385                                .and_then(|m| m.modified().ok())
1386                                .map(|t| {
1387                                    let duration =
1388                                        t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
1389                                    format!("{}s since epoch", duration.as_secs())
1390                                })
1391                                .unwrap_or_else(|| "unknown".to_string());
1392                            archived.push((name, modified));
1393                        }
1394                    }
1395                }
1396
1397                if archived.is_empty() {
1398                    println!("Archived: (none)");
1399                } else {
1400                    archived.sort_by(|a, b| b.1.cmp(&a.1));
1401                    for (name, modified) in &archived {
1402                        println!("Archived: {}/{} (modified: {})", key_dir, name, modified);
1403                    }
1404                }
1405            }
1406            Some(("repair", sub_m)) => {
1407                use jacs::keystore::RotationJournal;
1408                use jacs::simple::SimpleAgent;
1409
1410                let config_path = sub_m.get_one::<String>("config");
1411                let config_p = config_path
1412                    .map(|s| s.as_str())
1413                    .unwrap_or("./jacs.config.json");
1414
1415                let config =
1416                    jacs::config::Config::from_file(config_p).map_err(|e| -> Box<dyn Error> {
1417                        Box::new(std::io::Error::new(
1418                            std::io::ErrorKind::Other,
1419                            format!("Failed to load config: {}", e),
1420                        ))
1421                    })?;
1422
1423                let key_dir = config
1424                    .jacs_key_directory()
1425                    .as_deref()
1426                    .unwrap_or("./jacs_keys");
1427
1428                let journal_path = RotationJournal::journal_path(key_dir);
1429                if RotationJournal::load(&journal_path).is_some() {
1430                    println!(
1431                        "Incomplete rotation detected (journal at {}). Loading agent to trigger auto-repair...",
1432                        journal_path
1433                    );
1434                    // Loading the agent triggers warn_if_config_tampered -> auto-repair
1435                    let _agent =
1436                        SimpleAgent::load(Some(config_p), None).map_err(|e| -> Box<dyn Error> {
1437                            Box::new(std::io::Error::new(
1438                                std::io::ErrorKind::Other,
1439                                format!("Repair failed: {}", e),
1440                            ))
1441                        })?;
1442                    // Check if journal was cleaned up
1443                    if RotationJournal::load(&journal_path).is_none() {
1444                        println!("Config repaired successfully. Journal cleaned up.");
1445                    } else {
1446                        println!(
1447                            "Warning: Journal still present after load. Manual intervention may be needed."
1448                        );
1449                    }
1450                } else {
1451                    println!("No incomplete rotation detected. Nothing to repair.");
1452                }
1453            }
1454            _ => println!("please enter subcommand see jacs agent --help"),
1455        },
1456
1457        Some(("task", task_matches)) => match task_matches.subcommand() {
1458            Some(("create", create_matches)) => {
1459                let _agentfile = create_matches.get_one::<String>("agent-file");
1460                let mut agent: Agent = load_agent().expect("failed to load agent for task create");
1461                let name = create_matches
1462                    .get_one::<String>("name")
1463                    .expect("task name is required");
1464                let description = create_matches
1465                    .get_one::<String>("description")
1466                    .expect("task description is required");
1467                println!(
1468                    "{}",
1469                    create_task(&mut agent, name.to_string(), description.to_string()).unwrap()
1470                );
1471            }
1472            Some(("update", update_matches)) => {
1473                let mut agent: Agent = load_agent().expect("failed to load agent for task update");
1474                let task_key = update_matches
1475                    .get_one::<String>("task-key")
1476                    .expect("task key is required");
1477                let filename = update_matches
1478                    .get_one::<String>("filename")
1479                    .expect("filename is required");
1480                let updated_json = std::fs::read_to_string(filename)
1481                    .unwrap_or_else(|e| panic!("Failed to read '{}': {}", filename, e));
1482                println!(
1483                    "{}",
1484                    jacs::update_task(&mut agent, task_key, &updated_json).unwrap()
1485                );
1486            }
1487            _ => println!("please enter subcommand see jacs task --help"),
1488        },
1489        Some(("document", document_matches)) => match document_matches.subcommand() {
1490            Some(("create", create_matches)) => {
1491                let filename = create_matches.get_one::<String>("filename");
1492                let outputfilename = create_matches.get_one::<String>("output");
1493                let directory = create_matches.get_one::<String>("directory");
1494                let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1495                let no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1496                let _agentfile = create_matches.get_one::<String>("agent-file");
1497                let schema = create_matches.get_one::<String>("schema");
1498                let attachments = create_matches
1499                    .get_one::<String>("attach")
1500                    .map(|s| s.as_str());
1501                let embed: Option<bool> = create_matches.get_one::<bool>("embed").copied();
1502
1503                let mut agent: Agent = load_agent().expect("REASON");
1504
1505                let _attachment_links = agent.parse_attachement_arg(attachments);
1506                let _ = create_documents(
1507                    &mut agent,
1508                    filename,
1509                    directory,
1510                    outputfilename,
1511                    attachments,
1512                    embed,
1513                    no_save,
1514                    schema,
1515                );
1516            }
1517            // TODO copy for sharing
1518            // Some(("copy", create_matches)) => {
1519            Some(("update", create_matches)) => {
1520                let new_filename = create_matches.get_one::<String>("new").unwrap();
1521                let original_filename = create_matches.get_one::<String>("filename").unwrap();
1522                let outputfilename = create_matches.get_one::<String>("output");
1523                let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1524                let no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1525                let _agentfile = create_matches.get_one::<String>("agent-file");
1526                let schema = create_matches.get_one::<String>("schema");
1527                let attachments = create_matches
1528                    .get_one::<String>("attach")
1529                    .map(|s| s.as_str());
1530                let embed: Option<bool> = create_matches.get_one::<bool>("embed").copied();
1531
1532                let mut agent: Agent = load_agent().expect("REASON");
1533
1534                let attachment_links = agent.parse_attachement_arg(attachments);
1535                update_documents(
1536                    &mut agent,
1537                    new_filename,
1538                    original_filename,
1539                    outputfilename,
1540                    attachment_links,
1541                    embed,
1542                    no_save,
1543                    schema,
1544                )?;
1545            }
1546            Some(("sign-agreement", create_matches)) => {
1547                let filename = create_matches.get_one::<String>("filename");
1548                let directory = create_matches.get_one::<String>("directory");
1549                let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1550                let _agentfile = create_matches.get_one::<String>("agent-file");
1551                let mut agent: Agent = load_agent().expect("REASON");
1552                let schema = create_matches.get_one::<String>("schema");
1553                let _no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1554
1555                // Use updated set_file_list with storage
1556                sign_documents(&mut agent, schema, filename, directory)?;
1557            }
1558            Some(("check-agreement", create_matches)) => {
1559                let filename = create_matches.get_one::<String>("filename");
1560                let directory = create_matches.get_one::<String>("directory");
1561                let _agentfile = create_matches.get_one::<String>("agent-file");
1562                let mut agent: Agent = load_agent().expect("REASON");
1563                let schema = create_matches.get_one::<String>("schema");
1564
1565                // Use updated set_file_list with storage
1566                let _files: Vec<String> = default_set_file_list(filename, directory, None)
1567                    .expect("Failed to determine file list");
1568                check_agreement(&mut agent, schema, filename, directory)?;
1569            }
1570            Some(("create-agreement", create_matches)) => {
1571                let filename = create_matches.get_one::<String>("filename");
1572                let directory = create_matches.get_one::<String>("directory");
1573                let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1574                let _agentfile = create_matches.get_one::<String>("agent-file");
1575
1576                let schema = create_matches.get_one::<String>("schema");
1577                let no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1578                let agentids: Vec<String> = create_matches // Corrected reference to create_matches
1579                    .get_many::<String>("agentids")
1580                    .unwrap_or_default()
1581                    .map(|s| s.to_string())
1582                    .collect();
1583
1584                let mut agent: Agent = load_agent().expect("REASON");
1585                // Use updated set_file_list with storage
1586                let _ =
1587                    create_agreement(&mut agent, agentids, filename, schema, no_save, directory);
1588            }
1589
1590            Some(("verify", verify_matches)) => {
1591                let filename = verify_matches.get_one::<String>("filename");
1592                let directory = verify_matches.get_one::<String>("directory");
1593                let _verbose = *verify_matches.get_one::<bool>("verbose").unwrap_or(&false);
1594                let _agentfile = verify_matches.get_one::<String>("agent-file");
1595                let mut agent: Agent = load_agent().expect("REASON");
1596                let schema = verify_matches.get_one::<String>("schema");
1597                // Use updated set_file_list with storage
1598                verify_documents(&mut agent, schema, filename, directory)?;
1599            }
1600
1601            Some(("extract", extract_matches)) => {
1602                let filename = extract_matches.get_one::<String>("filename");
1603                let directory = extract_matches.get_one::<String>("directory");
1604                let _verbose = *extract_matches.get_one::<bool>("verbose").unwrap_or(&false);
1605                let _agentfile = extract_matches.get_one::<String>("agent-file");
1606                let mut agent: Agent = load_agent().expect("REASON");
1607                let schema = extract_matches.get_one::<String>("schema");
1608                // Use updated set_file_list with storage
1609                let _files: Vec<String> = default_set_file_list(filename, directory, None)
1610                    .expect("Failed to determine file list");
1611                // extract the contents but do not save
1612                extract_documents(&mut agent, schema, filename, directory)?;
1613            }
1614
1615            _ => println!("please enter subcommand see jacs document --help"),
1616        },
1617        Some(("key", key_matches)) => match key_matches.subcommand() {
1618            Some(("reencrypt", _reencrypt_matches)) => {
1619                use jacs::crypt::aes_encrypt::password_requirements;
1620                use jacs::simple::SimpleAgent;
1621
1622                // Load the agent first to find the key file
1623                let agent = SimpleAgent::load(None, None).map_err(|e| -> Box<dyn Error> {
1624                    Box::new(std::io::Error::new(
1625                        std::io::ErrorKind::Other,
1626                        format!("Failed to load agent: {}", e),
1627                    ))
1628                })?;
1629
1630                println!("Re-encrypting private key.\n");
1631
1632                // Get old password
1633                println!("Enter current password:");
1634                let old_password = read_password().map_err(|e| -> Box<dyn Error> {
1635                    Box::new(std::io::Error::new(
1636                        std::io::ErrorKind::Other,
1637                        format!("Error reading password: {}", e),
1638                    ))
1639                })?;
1640
1641                if old_password.is_empty() {
1642                    eprintln!("Error: current password cannot be empty.");
1643                    process::exit(1);
1644                }
1645
1646                // Get new password
1647                println!("\n{}", password_requirements());
1648                println!("\nEnter new password:");
1649                let new_password = read_password().map_err(|e| -> Box<dyn Error> {
1650                    Box::new(std::io::Error::new(
1651                        std::io::ErrorKind::Other,
1652                        format!("Error reading password: {}", e),
1653                    ))
1654                })?;
1655
1656                println!("Confirm new password:");
1657                let new_password_confirm = read_password().map_err(|e| -> Box<dyn Error> {
1658                    Box::new(std::io::Error::new(
1659                        std::io::ErrorKind::Other,
1660                        format!("Error reading password: {}", e),
1661                    ))
1662                })?;
1663
1664                if new_password != new_password_confirm {
1665                    eprintln!("Error: new passwords do not match.");
1666                    process::exit(1);
1667                }
1668
1669                jacs::simple::advanced::reencrypt_key(&agent, &old_password, &new_password)
1670                    .map_err(|e| -> Box<dyn Error> {
1671                        Box::new(std::io::Error::new(
1672                            std::io::ErrorKind::Other,
1673                            format!("Re-encryption failed: {}", e),
1674                        ))
1675                    })?;
1676
1677                println!("Private key re-encrypted successfully.");
1678            }
1679            _ => println!("please enter subcommand see jacs key --help"),
1680        },
1681        #[cfg(feature = "mcp")]
1682        Some(("mcp", mcp_matches)) => match mcp_matches.subcommand() {
1683            Some(("install", _)) => {
1684                eprintln!("`jacs mcp install` is no longer needed.");
1685                eprintln!("MCP is built into the jacs binary. Use `jacs mcp` to serve.");
1686                process::exit(0);
1687            }
1688            Some(("run", _)) => {
1689                eprintln!("`jacs mcp run` is no longer needed.");
1690                eprintln!("Use `jacs mcp` directly to start the MCP server.");
1691                process::exit(0);
1692            }
1693            _ => {
1694                let profile_str = mcp_matches.get_one::<String>("profile").map(|s| s.as_str());
1695                let profile = jacs_mcp::Profile::resolve(profile_str);
1696                let (agent, info) = jacs_mcp::load_agent_from_config_env_with_info()?;
1697                let state_roots = info["data_directory"]
1698                    .as_str()
1699                    .map(std::path::PathBuf::from)
1700                    .into_iter()
1701                    .collect();
1702                let server = jacs_mcp::JacsMcpServer::with_profile_and_state_roots(
1703                    agent,
1704                    profile,
1705                    state_roots,
1706                );
1707                let rt = tokio::runtime::Runtime::new()?;
1708                rt.block_on(jacs_mcp::serve_stdio(server))?;
1709            }
1710        },
1711        #[cfg(not(feature = "mcp"))]
1712        Some(("mcp", _)) => {
1713            eprintln!(
1714                "MCP support not compiled. Install with default features: cargo install jacs-cli"
1715            );
1716            process::exit(1);
1717        }
1718        Some(("a2a", a2a_matches)) => match a2a_matches.subcommand() {
1719            Some(("assess", assess_matches)) => {
1720                use jacs::a2a::AgentCard;
1721                use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
1722
1723                let source = assess_matches.get_one::<String>("source").unwrap();
1724                let policy_str = assess_matches
1725                    .get_one::<String>("policy")
1726                    .map(|s| s.as_str())
1727                    .unwrap_or("verified");
1728                let json_output = *assess_matches.get_one::<bool>("json").unwrap_or(&false);
1729
1730                let policy = A2ATrustPolicy::from_str_loose(policy_str)
1731                    .map_err(|e| Box::<dyn Error>::from(format!("Invalid policy: {}", e)))?;
1732
1733                // Load the Agent Card from file or URL
1734                let card_json = if source.starts_with("http://") || source.starts_with("https://") {
1735                    let client = reqwest::blocking::Client::builder()
1736                        .timeout(std::time::Duration::from_secs(10))
1737                        .build()
1738                        .map_err(|e| format!("HTTP client error: {}", e))?;
1739                    client
1740                        .get(source.as_str())
1741                        .send()
1742                        .map_err(|e| format!("Fetch failed: {}", e))?
1743                        .text()
1744                        .map_err(|e| format!("Read body failed: {}", e))?
1745                } else {
1746                    std::fs::read_to_string(source)
1747                        .map_err(|e| format!("Read file failed: {}", e))?
1748                };
1749
1750                let card: AgentCard = serde_json::from_str(&card_json)
1751                    .map_err(|e| format!("Invalid Agent Card JSON: {}", e))?;
1752
1753                // Create an empty agent for assessment context
1754                let agent = jacs::get_empty_agent();
1755                let assessment = assess_a2a_agent(&agent, &card, policy);
1756
1757                if json_output {
1758                    println!(
1759                        "{}",
1760                        serde_json::to_string_pretty(&assessment)
1761                            .expect("assessment serialization")
1762                    );
1763                } else {
1764                    println!("Agent:       {}", card.name);
1765                    println!(
1766                        "Agent ID:    {}",
1767                        assessment.agent_id.as_deref().unwrap_or("(not specified)")
1768                    );
1769                    println!("Policy:      {}", assessment.policy);
1770                    println!("Trust Level: {}", assessment.trust_level);
1771                    println!(
1772                        "Allowed:     {}",
1773                        if assessment.allowed { "YES" } else { "NO" }
1774                    );
1775                    println!("JACS Ext:    {}", assessment.jacs_registered);
1776                    println!("Reason:      {}", assessment.reason);
1777                    if !assessment.allowed {
1778                        process::exit(1);
1779                    }
1780                }
1781            }
1782            Some(("trust", trust_matches)) => {
1783                use jacs::a2a::AgentCard;
1784                use jacs::trust;
1785
1786                let source = trust_matches.get_one::<String>("source").unwrap();
1787
1788                // Load the Agent Card from file or URL
1789                let card_json = if source.starts_with("http://") || source.starts_with("https://") {
1790                    let client = reqwest::blocking::Client::builder()
1791                        .timeout(std::time::Duration::from_secs(10))
1792                        .build()
1793                        .map_err(|e| format!("HTTP client error: {}", e))?;
1794                    client
1795                        .get(source.as_str())
1796                        .send()
1797                        .map_err(|e| format!("Fetch failed: {}", e))?
1798                        .text()
1799                        .map_err(|e| format!("Read body failed: {}", e))?
1800                } else {
1801                    std::fs::read_to_string(source)
1802                        .map_err(|e| format!("Read file failed: {}", e))?
1803                };
1804
1805                let card: AgentCard = serde_json::from_str(&card_json)
1806                    .map_err(|e| format!("Invalid Agent Card JSON: {}", e))?;
1807
1808                // Extract agent ID and version from metadata
1809                let agent_id = card
1810                    .metadata
1811                    .as_ref()
1812                    .and_then(|m| m.get("jacsId"))
1813                    .and_then(|v| v.as_str())
1814                    .ok_or("Agent Card has no jacsId in metadata")?;
1815                let agent_version = card
1816                    .metadata
1817                    .as_ref()
1818                    .and_then(|m| m.get("jacsVersion"))
1819                    .and_then(|v| v.as_str())
1820                    .unwrap_or("unknown");
1821
1822                let key = format!("{}:{}", agent_id, agent_version);
1823
1824                // Add to trust store with the card JSON as the public key PEM
1825                // (the trust store stores agent metadata; public key comes from
1826                //  well-known endpoints or DNS in practice)
1827                trust::trust_a2a_card(&key, &card_json)?;
1828
1829                println!(
1830                    "Saved unverified A2A Agent Card bookmark '{}' ({})",
1831                    card.name, agent_id
1832                );
1833                println!("  Version: {}", agent_version);
1834                println!("  Bookmark key: {}", key);
1835                println!(
1836                    "  This entry is not cryptographically trusted until verified JACS identity material is added."
1837                );
1838            }
1839            Some(("discover", discover_matches)) => {
1840                use jacs::a2a::AgentCard;
1841                use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
1842
1843                let base_url = discover_matches.get_one::<String>("url").unwrap();
1844                let json_output = *discover_matches.get_one::<bool>("json").unwrap_or(&false);
1845                let policy_str = discover_matches
1846                    .get_one::<String>("policy")
1847                    .map(|s| s.as_str())
1848                    .unwrap_or("verified");
1849
1850                let policy = A2ATrustPolicy::from_str_loose(policy_str)
1851                    .map_err(|e| Box::<dyn Error>::from(format!("Invalid policy: {}", e)))?;
1852
1853                // Construct the well-known URL
1854                let trimmed = base_url.trim_end_matches('/');
1855                let card_url = format!("{}/.well-known/agent-card.json", trimmed);
1856
1857                let client = reqwest::blocking::Client::builder()
1858                    .timeout(std::time::Duration::from_secs(10))
1859                    .build()
1860                    .map_err(|e| format!("HTTP client error: {}", e))?;
1861
1862                let response = client
1863                    .get(&card_url)
1864                    .send()
1865                    .map_err(|e| format!("Failed to fetch {}: {}", card_url, e))?;
1866
1867                if !response.status().is_success() {
1868                    eprintln!(
1869                        "Failed to discover agent at {}: HTTP {}",
1870                        card_url,
1871                        response.status()
1872                    );
1873                    process::exit(1);
1874                }
1875
1876                let card_json = response
1877                    .text()
1878                    .map_err(|e| format!("Read body failed: {}", e))?;
1879
1880                let card: AgentCard = serde_json::from_str(&card_json)
1881                    .map_err(|e| format!("Invalid Agent Card JSON at {}: {}", card_url, e))?;
1882
1883                if json_output {
1884                    // Print the full Agent Card
1885                    println!(
1886                        "{}",
1887                        serde_json::to_string_pretty(&card).expect("card serialization")
1888                    );
1889                } else {
1890                    // Human-readable summary
1891                    println!("Discovered A2A Agent: {}", card.name);
1892                    println!("  Description: {}", card.description);
1893                    println!("  Version:     {}", card.version);
1894                    println!("  Protocol:    {}", card.protocol_versions.join(", "));
1895
1896                    // Show interfaces
1897                    for iface in &card.supported_interfaces {
1898                        println!("  Endpoint:    {} ({})", iface.url, iface.protocol_binding);
1899                    }
1900
1901                    // Show skills
1902                    if !card.skills.is_empty() {
1903                        println!("  Skills:");
1904                        for skill in &card.skills {
1905                            println!("    - {} ({})", skill.name, skill.id);
1906                        }
1907                    }
1908
1909                    // JACS extension check
1910                    let has_jacs = card
1911                        .capabilities
1912                        .extensions
1913                        .as_ref()
1914                        .map(|exts| exts.iter().any(|e| e.uri == jacs::a2a::JACS_EXTENSION_URI))
1915                        .unwrap_or(false);
1916                    println!("  JACS:        {}", if has_jacs { "YES" } else { "NO" });
1917
1918                    // Trust assessment
1919                    let agent = jacs::get_empty_agent();
1920                    let assessment = assess_a2a_agent(&agent, &card, policy);
1921                    println!(
1922                        "  Trust:       {} ({})",
1923                        assessment.trust_level, assessment.reason
1924                    );
1925                    if !assessment.allowed {
1926                        println!(
1927                            "  WARNING:     Agent not allowed under '{}' policy",
1928                            policy_str
1929                        );
1930                    }
1931                }
1932            }
1933            Some(("serve", serve_matches)) => {
1934                let port = *serve_matches.get_one::<u16>("port").unwrap();
1935                let host = serve_matches
1936                    .get_one::<String>("host")
1937                    .map(|s| s.as_str())
1938                    .unwrap_or("127.0.0.1");
1939
1940                // Load or quickstart the agent
1941                ensure_cli_private_key_password().map_err(|e| -> Box<dyn Error> {
1942                    Box::new(std::io::Error::other(format!(
1943                        "Password bootstrap failed: {}\n\n{}",
1944                        e,
1945                        quickstart_password_bootstrap_help()
1946                    )))
1947                })?;
1948                let (agent, info) = jacs::simple::advanced::quickstart(
1949                    "jacs-agent",
1950                    "localhost",
1951                    Some("JACS A2A agent"),
1952                    None,
1953                    None,
1954                )
1955                .map_err(|e| wrap_quickstart_error_with_password_help("Failed to load agent", e))?;
1956
1957                // Export the Agent Card for display
1958                let agent_card = jacs::a2a::simple::export_agent_card(&agent).map_err(
1959                    |e| -> Box<dyn Error> {
1960                        Box::new(std::io::Error::new(
1961                            std::io::ErrorKind::Other,
1962                            format!("Failed to export Agent Card: {}", e),
1963                        ))
1964                    },
1965                )?;
1966
1967                // Generate well-known documents via public API
1968                let documents = jacs::a2a::simple::generate_well_known_documents(&agent, None)
1969                    .map_err(|e| -> Box<dyn Error> {
1970                        Box::new(std::io::Error::new(
1971                            std::io::ErrorKind::Other,
1972                            format!("Failed to generate well-known documents: {}", e),
1973                        ))
1974                    })?;
1975
1976                // Build a lookup map: path -> JSON body
1977                let mut routes: std::collections::HashMap<String, String> =
1978                    std::collections::HashMap::new();
1979                for (path, value) in &documents {
1980                    routes.insert(
1981                        path.clone(),
1982                        serde_json::to_string_pretty(value).unwrap_or_default(),
1983                    );
1984                }
1985
1986                let addr = format!("{}:{}", host, port);
1987                let server = tiny_http::Server::http(&addr)
1988                    .map_err(|e| format!("Failed to start server on {}: {}", addr, e))?;
1989
1990                println!("Serving A2A well-known endpoints at http://{}", addr);
1991                println!("  Agent: {} ({})", agent_card.name, info.agent_id);
1992                println!("  Endpoints:");
1993                for path in routes.keys() {
1994                    println!("    http://{}{}", addr, path);
1995                }
1996                println!("\nPress Ctrl+C to stop.");
1997
1998                for request in server.incoming_requests() {
1999                    let url = request.url().to_string();
2000                    if let Some(body) = routes.get(&url) {
2001                        let response = tiny_http::Response::from_string(body.clone()).with_header(
2002                            tiny_http::Header::from_bytes(
2003                                &b"Content-Type"[..],
2004                                &b"application/json"[..],
2005                            )
2006                            .unwrap(),
2007                        );
2008                        let _ = request.respond(response);
2009                    } else {
2010                        let response =
2011                            tiny_http::Response::from_string("{\"error\": \"not found\"}")
2012                                .with_status_code(404)
2013                                .with_header(
2014                                    tiny_http::Header::from_bytes(
2015                                        &b"Content-Type"[..],
2016                                        &b"application/json"[..],
2017                                    )
2018                                    .unwrap(),
2019                                );
2020                        let _ = request.respond(response);
2021                    }
2022                }
2023            }
2024            Some(("quickstart", qs_matches)) => {
2025                let port = *qs_matches.get_one::<u16>("port").unwrap();
2026                let host = qs_matches
2027                    .get_one::<String>("host")
2028                    .map(|s| s.as_str())
2029                    .unwrap_or("127.0.0.1");
2030                let algorithm = qs_matches
2031                    .get_one::<String>("algorithm")
2032                    .map(|s| s.as_str());
2033                let name = qs_matches
2034                    .get_one::<String>("name")
2035                    .map(|s| s.as_str())
2036                    .unwrap_or("jacs-agent");
2037                let domain = qs_matches
2038                    .get_one::<String>("domain")
2039                    .map(|s| s.as_str())
2040                    .unwrap_or("localhost");
2041                let description = qs_matches
2042                    .get_one::<String>("description")
2043                    .map(|s| s.as_str());
2044
2045                // Create or load the agent via quickstart
2046                ensure_cli_private_key_password().map_err(|e| -> Box<dyn Error> {
2047                    Box::new(std::io::Error::other(format!(
2048                        "Password bootstrap failed: {}\n\n{}",
2049                        e,
2050                        quickstart_password_bootstrap_help()
2051                    )))
2052                })?;
2053                let (agent, info) =
2054                    jacs::simple::advanced::quickstart(name, domain, description, algorithm, None)
2055                        .map_err(|e| {
2056                            wrap_quickstart_error_with_password_help(
2057                                "Failed to quickstart agent",
2058                                e,
2059                            )
2060                        })?;
2061
2062                // Export the Agent Card
2063                let agent_card = jacs::a2a::simple::export_agent_card(&agent).map_err(
2064                    |e| -> Box<dyn Error> {
2065                        Box::new(std::io::Error::new(
2066                            std::io::ErrorKind::Other,
2067                            format!("Failed to export Agent Card: {}", e),
2068                        ))
2069                    },
2070                )?;
2071
2072                // Generate well-known documents
2073                let documents = jacs::a2a::simple::generate_well_known_documents(&agent, None)
2074                    .map_err(|e| -> Box<dyn Error> {
2075                        Box::new(std::io::Error::new(
2076                            std::io::ErrorKind::Other,
2077                            format!("Failed to generate well-known documents: {}", e),
2078                        ))
2079                    })?;
2080
2081                // Build route map
2082                let mut routes: std::collections::HashMap<String, String> =
2083                    std::collections::HashMap::new();
2084                for (path, value) in &documents {
2085                    routes.insert(
2086                        path.clone(),
2087                        serde_json::to_string_pretty(value).unwrap_or_default(),
2088                    );
2089                }
2090
2091                let addr = format!("{}:{}", host, port);
2092                let server = tiny_http::Server::http(&addr)
2093                    .map_err(|e| format!("Failed to start server on {}: {}", addr, e))?;
2094
2095                println!("A2A Quickstart");
2096                println!("==============");
2097                println!("Agent: {} ({})", agent_card.name, info.agent_id);
2098                println!("Algorithm: {}", algorithm.unwrap_or("pq2025"));
2099                println!();
2100                println!("Discovery URL: http://{}/.well-known/agent-card.json", addr);
2101                println!();
2102                println!("Endpoints:");
2103                for path in routes.keys() {
2104                    println!("  http://{}{}", addr, path);
2105                }
2106                println!();
2107                println!("Press Ctrl+C to stop.");
2108
2109                for request in server.incoming_requests() {
2110                    let url = request.url().to_string();
2111                    if let Some(body) = routes.get(&url) {
2112                        let response = tiny_http::Response::from_string(body.clone()).with_header(
2113                            tiny_http::Header::from_bytes(
2114                                &b"Content-Type"[..],
2115                                &b"application/json"[..],
2116                            )
2117                            .unwrap(),
2118                        );
2119                        let _ = request.respond(response);
2120                    } else {
2121                        let response =
2122                            tiny_http::Response::from_string("{\"error\": \"not found\"}")
2123                                .with_status_code(404)
2124                                .with_header(
2125                                    tiny_http::Header::from_bytes(
2126                                        &b"Content-Type"[..],
2127                                        &b"application/json"[..],
2128                                    )
2129                                    .unwrap(),
2130                                );
2131                        let _ = request.respond(response);
2132                    }
2133                }
2134            }
2135            _ => println!("please enter subcommand see jacs a2a --help"),
2136        },
2137        Some(("quickstart", qs_matches)) => {
2138            let algorithm = qs_matches
2139                .get_one::<String>("algorithm")
2140                .map(|s| s.as_str());
2141            let name = qs_matches
2142                .get_one::<String>("name")
2143                .map(|s| s.as_str())
2144                .unwrap_or("jacs-agent");
2145            let domain = qs_matches
2146                .get_one::<String>("domain")
2147                .map(|s| s.as_str())
2148                .unwrap_or("localhost");
2149            let description = qs_matches
2150                .get_one::<String>("description")
2151                .map(|s| s.as_str());
2152            let do_sign = *qs_matches.get_one::<bool>("sign").unwrap_or(&false);
2153            let sign_file = qs_matches.get_one::<String>("file");
2154
2155            // Try to resolve password from existing sources (env var, password file, legacy file).
2156            // If none found, prompt interactively and store in OS keychain.
2157            if let Err(e) = ensure_cli_private_key_password() {
2158                eprintln!("Note: {}", e);
2159            }
2160
2161            // If still no password available, prompt interactively
2162            if env::var("JACS_PRIVATE_KEY_PASSWORD")
2163                .unwrap_or_default()
2164                .trim()
2165                .is_empty()
2166            {
2167                eprintln!("{}", jacs::crypt::aes_encrypt::password_requirements());
2168                let password = loop {
2169                    eprintln!("Enter a password for your JACS private key:");
2170                    let pw =
2171                        read_password().map_err(|e| format!("Failed to read password: {}", e))?;
2172                    if pw.trim().is_empty() {
2173                        eprintln!("Password cannot be empty. Please try again.");
2174                        continue;
2175                    }
2176                    eprintln!("Confirm password:");
2177                    let pw2 =
2178                        read_password().map_err(|e| format!("Failed to read password: {}", e))?;
2179                    if pw != pw2 {
2180                        eprintln!("Passwords do not match. Please try again.");
2181                        continue;
2182                    }
2183                    break pw;
2184                };
2185
2186                // SAFETY: CLI is single-threaded at this point
2187                unsafe {
2188                    env::set_var("JACS_PRIVATE_KEY_PASSWORD", &password);
2189                }
2190
2191                // Note: keychain storage is handled by quickstart() after agent
2192                // creation, when the agent_id is known.
2193            }
2194
2195            let (agent, info) =
2196                jacs::simple::advanced::quickstart(name, domain, description, algorithm, None)
2197                    .map_err(|e| {
2198                        wrap_quickstart_error_with_password_help("Quickstart failed", e)
2199                    })?;
2200
2201            if do_sign {
2202                // Sign mode: read JSON, sign it, print signed document
2203                let input = if let Some(file_path) = sign_file {
2204                    std::fs::read_to_string(file_path)?
2205                } else {
2206                    use std::io::Read;
2207                    let mut buf = String::new();
2208                    std::io::stdin().read_to_string(&mut buf)?;
2209                    buf
2210                };
2211
2212                let value: serde_json::Value = serde_json::from_str(&input)
2213                    .map_err(|e| format!("Invalid JSON input: {}", e))?;
2214
2215                let signed = agent.sign_message(&value).map_err(|e| -> Box<dyn Error> {
2216                    Box::new(std::io::Error::new(
2217                        std::io::ErrorKind::Other,
2218                        format!("Signing failed: {}", e),
2219                    ))
2220                })?;
2221
2222                println!("{}", signed.raw);
2223            } else {
2224                // Info mode: print agent details
2225                println!("JACS agent ready ({})", info.algorithm);
2226                println!("  Agent ID: {}", info.agent_id);
2227                println!("  Version:  {}", info.version);
2228                println!("  Config:   {}", info.config_path);
2229                println!("  Keys:     {}", info.key_directory);
2230                println!();
2231                println!("Sign something:");
2232                println!("  echo '{{\"hello\":\"world\"}}' | jacs quickstart --sign");
2233            }
2234        }
2235        #[cfg(feature = "attestation")]
2236        Some(("attest", attest_matches)) => {
2237            use jacs::attestation::types::*;
2238            use jacs::simple::SimpleAgent;
2239
2240            match attest_matches.subcommand() {
2241                Some(("create", create_matches)) => {
2242                    // Ensure password is available for signing
2243                    ensure_cli_private_key_password()?;
2244
2245                    // Load agent
2246                    let agent = match SimpleAgent::load(None, None) {
2247                        Ok(a) => a,
2248                        Err(e) => {
2249                            eprintln!("Failed to load agent: {}", e);
2250                            eprintln!("Run `jacs quickstart` first to create an agent.");
2251                            process::exit(1);
2252                        }
2253                    };
2254
2255                    // Parse claims (required)
2256                    let claims_str = create_matches
2257                        .get_one::<String>("claims")
2258                        .expect("claims is required");
2259                    let claims: Vec<Claim> = serde_json::from_str(claims_str).map_err(|e| {
2260                        format!(
2261                            "Invalid claims JSON: {}. \
2262                             Provide a JSON array like '[{{\"name\":\"reviewed\",\"value\":true}}]'",
2263                            e
2264                        )
2265                    })?;
2266
2267                    // Parse optional evidence
2268                    let evidence: Vec<EvidenceRef> =
2269                        if let Some(ev_str) = create_matches.get_one::<String>("evidence") {
2270                            serde_json::from_str(ev_str)
2271                                .map_err(|e| format!("Invalid evidence JSON: {}", e))?
2272                        } else {
2273                            vec![]
2274                        };
2275
2276                    let att_json = if let Some(doc_path) =
2277                        create_matches.get_one::<String>("from-document")
2278                    {
2279                        // Lift from existing signed document
2280                        let doc_content = std::fs::read_to_string(doc_path).map_err(|e| {
2281                            format!("Failed to read document '{}': {}", doc_path, e)
2282                        })?;
2283                        let result = jacs::attestation::simple::lift(&agent, &doc_content, &claims)
2284                            .map_err(|e| {
2285                                format!("Failed to lift document to attestation: {}", e)
2286                            })?;
2287                        result.raw
2288                    } else {
2289                        // Build from scratch: need subject-type, subject-id, subject-digest
2290                        let subject_type_str = create_matches
2291                            .get_one::<String>("subject-type")
2292                            .ok_or("--subject-type is required when not using --from-document")?;
2293                        let subject_id = create_matches
2294                            .get_one::<String>("subject-id")
2295                            .ok_or("--subject-id is required when not using --from-document")?;
2296                        let subject_digest = create_matches
2297                            .get_one::<String>("subject-digest")
2298                            .ok_or("--subject-digest is required when not using --from-document")?;
2299
2300                        let subject_type = match subject_type_str.as_str() {
2301                            "agent" => SubjectType::Agent,
2302                            "artifact" => SubjectType::Artifact,
2303                            "workflow" => SubjectType::Workflow,
2304                            "identity" => SubjectType::Identity,
2305                            other => {
2306                                return Err(format!("Unknown subject type: '{}'", other).into());
2307                            }
2308                        };
2309
2310                        let subject = AttestationSubject {
2311                            subject_type,
2312                            id: subject_id.clone(),
2313                            digests: DigestSet {
2314                                sha256: subject_digest.clone(),
2315                                sha512: None,
2316                                additional: std::collections::HashMap::new(),
2317                            },
2318                        };
2319
2320                        let result = jacs::attestation::simple::create(
2321                            &agent, &subject, &claims, &evidence, None, None,
2322                        )
2323                        .map_err(|e| format!("Failed to create attestation: {}", e))?;
2324                        result.raw
2325                    };
2326
2327                    // Output to file or stdout
2328                    if let Some(output_path) = create_matches.get_one::<String>("output") {
2329                        std::fs::write(output_path, &att_json).map_err(|e| {
2330                            format!("Failed to write output file '{}': {}", output_path, e)
2331                        })?;
2332                        eprintln!("Attestation written to {}", output_path);
2333                    } else {
2334                        println!("{}", att_json);
2335                    }
2336                }
2337                Some(("verify", verify_matches)) => {
2338                    let file_path = verify_matches
2339                        .get_one::<String>("file")
2340                        .expect("file is required");
2341                    let full = *verify_matches.get_one::<bool>("full").unwrap_or(&false);
2342                    let json_output = *verify_matches.get_one::<bool>("json").unwrap_or(&false);
2343                    let key_dir = verify_matches.get_one::<String>("key-dir");
2344                    let max_depth = verify_matches.get_one::<u32>("max-depth");
2345
2346                    // Set key directory if specified
2347                    if let Some(kd) = key_dir {
2348                        // SAFETY: CLI is single-threaded at this point
2349                        unsafe { std::env::set_var("JACS_KEY_DIRECTORY", kd) };
2350                    }
2351
2352                    // Set max derivation depth if specified
2353                    if let Some(depth) = max_depth {
2354                        // SAFETY: CLI is single-threaded at this point
2355                        unsafe {
2356                            std::env::set_var("JACS_MAX_DERIVATION_DEPTH", depth.to_string())
2357                        };
2358                    }
2359
2360                    // Read the attestation file
2361                    let att_content = std::fs::read_to_string(file_path).map_err(|e| {
2362                        format!("Failed to read attestation file '{}': {}", file_path, e)
2363                    })?;
2364
2365                    // Load or create ephemeral agent for verification
2366                    ensure_cli_private_key_password().ok();
2367                    let agent = match SimpleAgent::load(None, None) {
2368                        Ok(a) => a,
2369                        Err(_) => {
2370                            let (a, _) = SimpleAgent::ephemeral(Some("ed25519"))
2371                                .map_err(|e| format!("Failed to create verifier: {}", e))?;
2372                            a
2373                        }
2374                    };
2375
2376                    // Load the attestation document into agent storage first
2377                    let att_value: serde_json::Value = serde_json::from_str(&att_content)
2378                        .map_err(|e| format!("Invalid attestation JSON: {}", e))?;
2379                    let doc_key = format!(
2380                        "{}:{}",
2381                        att_value["jacsId"].as_str().unwrap_or("unknown"),
2382                        att_value["jacsVersion"].as_str().unwrap_or("unknown")
2383                    );
2384
2385                    // We need to store the document so verify can find it by key.
2386                    // Use verify() which parses and stores the doc, then verify attestation.
2387                    let verify_result = agent.verify(&att_content);
2388                    if let Err(e) = &verify_result {
2389                        if json_output {
2390                            let out = serde_json::json!({
2391                                "valid": false,
2392                                "error": e.to_string(),
2393                            });
2394                            println!("{}", serde_json::to_string_pretty(&out).unwrap());
2395                        } else {
2396                            eprintln!("Verification error: {}", e);
2397                        }
2398                        process::exit(1);
2399                    }
2400
2401                    // Now do attestation-specific verification
2402                    let att_result = if full {
2403                        jacs::attestation::simple::verify_full(&agent, &doc_key)
2404                    } else {
2405                        jacs::attestation::simple::verify(&agent, &doc_key)
2406                    };
2407
2408                    match att_result {
2409                        Ok(r) => {
2410                            if json_output {
2411                                println!("{}", serde_json::to_string_pretty(&r).unwrap());
2412                            } else {
2413                                println!(
2414                                    "Status:    {}",
2415                                    if r.valid { "VALID" } else { "INVALID" }
2416                                );
2417                                println!(
2418                                    "Signature: {}",
2419                                    if r.crypto.signature_valid {
2420                                        "valid"
2421                                    } else {
2422                                        "INVALID"
2423                                    }
2424                                );
2425                                println!(
2426                                    "Hash:      {}",
2427                                    if r.crypto.hash_valid {
2428                                        "valid"
2429                                    } else {
2430                                        "INVALID"
2431                                    }
2432                                );
2433                                if !r.crypto.signer_id.is_empty() {
2434                                    println!("Signer:    {}", r.crypto.signer_id);
2435                                }
2436                                if !r.evidence.is_empty() {
2437                                    println!("Evidence:  {} items checked", r.evidence.len());
2438                                }
2439                                if !r.errors.is_empty() {
2440                                    for err in &r.errors {
2441                                        eprintln!("  Error: {}", err);
2442                                    }
2443                                }
2444                            }
2445                            if !r.valid {
2446                                process::exit(1);
2447                            }
2448                        }
2449                        Err(e) => {
2450                            if json_output {
2451                                let out = serde_json::json!({
2452                                    "valid": false,
2453                                    "error": e.to_string(),
2454                                });
2455                                println!("{}", serde_json::to_string_pretty(&out).unwrap());
2456                            } else {
2457                                eprintln!("Attestation verification error: {}", e);
2458                            }
2459                            process::exit(1);
2460                        }
2461                    }
2462                }
2463                Some(("export-dsse", export_matches)) => {
2464                    let file_path = export_matches
2465                        .get_one::<String>("file")
2466                        .expect("file argument required");
2467                    let output_path = export_matches.get_one::<String>("output");
2468
2469                    let attestation_json = std::fs::read_to_string(file_path).unwrap_or_else(|e| {
2470                        eprintln!("Cannot read {}: {}", file_path, e);
2471                        process::exit(1);
2472                    });
2473
2474                    let (_agent, _info) = SimpleAgent::ephemeral(Some("ring-Ed25519"))
2475                        .unwrap_or_else(|e| {
2476                            eprintln!("Failed to create agent: {}", e);
2477                            process::exit(1);
2478                        });
2479
2480                    match jacs::attestation::simple::export_dsse(&attestation_json) {
2481                        Ok(envelope_json) => {
2482                            if let Some(out_path) = output_path {
2483                                std::fs::write(out_path, &envelope_json).unwrap_or_else(|e| {
2484                                    eprintln!("Cannot write to {}: {}", out_path, e);
2485                                    process::exit(1);
2486                                });
2487                                println!("DSSE envelope written to {}", out_path);
2488                            } else {
2489                                println!("{}", envelope_json);
2490                            }
2491                        }
2492                        Err(e) => {
2493                            eprintln!("Failed to export DSSE envelope: {}", e);
2494                            process::exit(1);
2495                        }
2496                    }
2497                }
2498                _ => {
2499                    eprintln!(
2500                        "Use 'jacs attest create', 'jacs attest verify', or 'jacs attest export-dsse'. See --help."
2501                    );
2502                    process::exit(1);
2503                }
2504            }
2505        }
2506        Some(("verify", verify_matches)) => {
2507            use jacs::simple::SimpleAgent;
2508            use serde_json::json;
2509
2510            let file_path = verify_matches.get_one::<String>("file");
2511            let remote_url = verify_matches.get_one::<String>("remote");
2512            let json_output = *verify_matches.get_one::<bool>("json").unwrap_or(&false);
2513            let key_dir = verify_matches.get_one::<String>("key-dir");
2514
2515            // Optionally set key directory env var so the agent resolves keys from there
2516            if let Some(kd) = key_dir {
2517                // SAFETY: CLI is single-threaded at this point
2518                unsafe { std::env::set_var("JACS_KEY_DIRECTORY", kd) };
2519            }
2520
2521            // Get the document content
2522            let document = if let Some(url) = remote_url {
2523                let client = reqwest::blocking::Client::builder()
2524                    .timeout(std::time::Duration::from_secs(30))
2525                    .build()
2526                    .map_err(|e| format!("HTTP client error: {}", e))?;
2527                let resp = client
2528                    .get(url)
2529                    .send()
2530                    .map_err(|e| format!("Fetch failed: {}", e))?;
2531                if !resp.status().is_success() {
2532                    eprintln!("HTTP error: {}", resp.status());
2533                    process::exit(1);
2534                }
2535                resp.text()
2536                    .map_err(|e| format!("Read body failed: {}", e))?
2537            } else if let Some(path) = file_path {
2538                std::fs::read_to_string(path).map_err(|e| format!("Read file failed: {}", e))?
2539            } else {
2540                eprintln!("Provide a file path or --remote <url>");
2541                process::exit(1);
2542            };
2543
2544            // Try to load an existing agent (from config in cwd or env vars).
2545            // This gives access to the agent's own keys for verifying self-signed docs.
2546            // Fall back to an ephemeral agent if no config is available.
2547            let agent = if std::path::Path::new("./jacs.config.json").exists() {
2548                if let Err(e) = ensure_cli_private_key_password() {
2549                    eprintln!("Warning: Password bootstrap failed: {}", e);
2550                    eprintln!("{}", quickstart_password_bootstrap_help());
2551                }
2552                match SimpleAgent::load(None, None) {
2553                    Ok(a) => a,
2554                    Err(e) => {
2555                        let lower = e.to_string().to_lowercase();
2556                        if lower.contains("password")
2557                            || lower.contains("decrypt")
2558                            || lower.contains("private key")
2559                        {
2560                            eprintln!(
2561                                "Warning: Could not load local agent from ./jacs.config.json: {}",
2562                                e
2563                            );
2564                            eprintln!("{}", quickstart_password_bootstrap_help());
2565                        }
2566                        let (a, _) = SimpleAgent::ephemeral(Some("ed25519"))
2567                            .map_err(|e| format!("Failed to create verifier: {}", e))?;
2568                        a
2569                    }
2570                }
2571            } else {
2572                let (a, _) = SimpleAgent::ephemeral(Some("ed25519"))
2573                    .map_err(|e| format!("Failed to create verifier: {}", e))?;
2574                a
2575            };
2576
2577            match agent.verify(&document) {
2578                Ok(r) => {
2579                    if json_output {
2580                        let out = json!({
2581                            "valid": r.valid,
2582                            "signerId": r.signer_id,
2583                            "timestamp": r.timestamp,
2584                        });
2585                        println!("{}", serde_json::to_string_pretty(&out).unwrap());
2586                    } else {
2587                        println!("Status:    {}", if r.valid { "VALID" } else { "INVALID" });
2588                        println!(
2589                            "Signer:    {}",
2590                            if r.signer_id.is_empty() {
2591                                "(unknown)"
2592                            } else {
2593                                &r.signer_id
2594                            }
2595                        );
2596                        if !r.timestamp.is_empty() {
2597                            println!("Signed at: {}", r.timestamp);
2598                        }
2599                    }
2600                    if !r.valid {
2601                        process::exit(1);
2602                    }
2603                }
2604                Err(e) => {
2605                    if json_output {
2606                        let out = json!({
2607                            "valid": false,
2608                            "error": e.to_string(),
2609                        });
2610                        println!("{}", serde_json::to_string_pretty(&out).unwrap());
2611                    } else {
2612                        eprintln!("Verification error: {}", e);
2613                    }
2614                    process::exit(1);
2615                }
2616            }
2617        }
2618        Some(("convert", convert_matches)) => {
2619            use jacs::convert::{html_to_jacs, jacs_to_html, jacs_to_yaml, yaml_to_jacs};
2620
2621            let target_format = convert_matches.get_one::<String>("to").unwrap();
2622            let source_format = convert_matches.get_one::<String>("from");
2623            let file_path = convert_matches.get_one::<String>("file").unwrap();
2624            let output_path = convert_matches.get_one::<String>("output");
2625
2626            // Auto-detect source format from extension if not explicitly provided
2627            let is_stdin = file_path == "-";
2628            let detected_format = if let Some(fmt) = source_format {
2629                fmt.clone()
2630            } else if is_stdin {
2631                eprintln!(
2632                    "When reading from stdin (-f -), --from is required to specify the source format."
2633                );
2634                process::exit(1);
2635            } else {
2636                let ext = std::path::Path::new(file_path)
2637                    .extension()
2638                    .and_then(|e| e.to_str())
2639                    .unwrap_or("");
2640                match ext {
2641                    "json" => "json".to_string(),
2642                    "yaml" | "yml" => "yaml".to_string(),
2643                    "html" | "htm" => "html".to_string(),
2644                    _ => {
2645                        eprintln!(
2646                            "Cannot auto-detect format for extension '{}'. Use --from to specify.",
2647                            ext
2648                        );
2649                        process::exit(1);
2650                    }
2651                }
2652            };
2653
2654            // Read input (from file or stdin)
2655            let input = if is_stdin {
2656                let mut buf = String::new();
2657                std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)
2658                    .map_err(|e| format!("Failed to read from stdin: {}", e))?;
2659                buf
2660            } else {
2661                std::fs::read_to_string(file_path)
2662                    .map_err(|e| format!("Failed to read '{}': {}", file_path, e))?
2663            };
2664
2665            // Convert
2666            let output = match (detected_format.as_str(), target_format.as_str()) {
2667                ("json", "yaml") => jacs_to_yaml(&input).map_err(|e| format!("{}", e))?,
2668                ("yaml", "json") => yaml_to_jacs(&input).map_err(|e| format!("{}", e))?,
2669                ("json", "html") => jacs_to_html(&input).map_err(|e| format!("{}", e))?,
2670                ("html", "json") => html_to_jacs(&input).map_err(|e| format!("{}", e))?,
2671                ("yaml", "html") => {
2672                    let json = yaml_to_jacs(&input).map_err(|e| format!("{}", e))?;
2673                    jacs_to_html(&json).map_err(|e| format!("{}", e))?
2674                }
2675                ("html", "yaml") => {
2676                    let json = html_to_jacs(&input).map_err(|e| format!("{}", e))?;
2677                    jacs_to_yaml(&json).map_err(|e| format!("{}", e))?
2678                }
2679                (src, dst) if src == dst => {
2680                    // Same format -- just pass through
2681                    input
2682                }
2683                (src, dst) => {
2684                    eprintln!("Unsupported conversion: {} -> {}", src, dst);
2685                    process::exit(1);
2686                }
2687            };
2688
2689            // Write output
2690            if let Some(out_path) = output_path {
2691                std::fs::write(out_path, &output)
2692                    .map_err(|e| format!("Failed to write '{}': {}", out_path, e))?;
2693                eprintln!("Written to {}", out_path);
2694            } else {
2695                print!("{}", output);
2696            }
2697        }
2698        Some(("init", init_matches)) => {
2699            let auto_yes = *init_matches.get_one::<bool>("yes").unwrap_or(&false);
2700            println!("--- Running Config Creation ---");
2701            handle_config_create()?;
2702            println!("\n--- Running Agent Creation (with keys) ---");
2703            handle_agent_create_auto(None, true, auto_yes)?;
2704            println!("\n--- JACS Initialization Complete ---");
2705        }
2706        #[cfg(feature = "keychain")]
2707        Some(("keychain", keychain_matches)) => {
2708            use jacs::keystore::keychain;
2709
2710            match keychain_matches.subcommand() {
2711                Some(("set", sub)) => {
2712                    let agent_id = sub.get_one::<String>("agent-id").unwrap();
2713                    let password = if let Some(pw) = sub.get_one::<String>("password") {
2714                        pw.clone()
2715                    } else {
2716                        eprintln!("Enter password to store in keychain:");
2717                        read_password().map_err(|e| format!("Failed to read password: {}", e))?
2718                    };
2719                    if password.trim().is_empty() {
2720                        eprintln!("Error: password cannot be empty.");
2721                        process::exit(1);
2722                    }
2723                    // Validate password strength before storing
2724                    if let Err(e) = jacs::crypt::aes_encrypt::check_password_strength(&password) {
2725                        eprintln!("Error: {}", e);
2726                        process::exit(1);
2727                    }
2728                    keychain::store_password(agent_id, &password)?;
2729                    eprintln!("Password stored in OS keychain for agent {}.", agent_id);
2730                }
2731                Some(("get", sub)) => {
2732                    let agent_id = sub.get_one::<String>("agent-id").unwrap();
2733                    match keychain::get_password(agent_id)? {
2734                        Some(pw) => println!("{}", pw),
2735                        None => {
2736                            eprintln!("No password found in OS keychain for agent {}.", agent_id);
2737                            process::exit(1);
2738                        }
2739                    }
2740                }
2741                Some(("delete", sub)) => {
2742                    let agent_id = sub.get_one::<String>("agent-id").unwrap();
2743                    keychain::delete_password(agent_id)?;
2744                    eprintln!("Password removed from OS keychain for agent {}.", agent_id);
2745                }
2746                Some(("status", sub)) => {
2747                    let agent_id = sub.get_one::<String>("agent-id").unwrap();
2748                    if keychain::is_available() {
2749                        match keychain::get_password(agent_id) {
2750                            Ok(Some(_)) => {
2751                                eprintln!("Keychain backend: available");
2752                                eprintln!("Agent: {}", agent_id);
2753                                eprintln!("Password: stored");
2754                            }
2755                            Ok(None) => {
2756                                eprintln!("Keychain backend: available");
2757                                eprintln!("Agent: {}", agent_id);
2758                                eprintln!("Password: not stored");
2759                            }
2760                            Err(e) => {
2761                                eprintln!("Keychain backend: error ({})", e);
2762                            }
2763                        }
2764                    } else {
2765                        eprintln!("Keychain backend: not available (feature disabled)");
2766                    }
2767                }
2768                _ => {
2769                    eprintln!("Unknown keychain subcommand. Use: set, get, delete, status");
2770                    process::exit(1);
2771                }
2772            }
2773        }
2774        _ => {
2775            // This branch should ideally be unreachable after adding arg_required_else_help(true)
2776            eprintln!("Invalid command or no subcommand provided. Use --help for usage.");
2777            process::exit(1); // Exit with error if this branch is reached
2778        }
2779    }
2780
2781    Ok(())
2782}