1use clap::{Arg, ArgAction, Command, crate_name, value_parser};
8
9use crate::password_bootstrap::quickstart_password_bootstrap_help;
10
11pub fn build_cli() -> Command {
12 let cmd = Command::new(crate_name!())
13 .version(env!("CARGO_PKG_VERSION"))
14 .about(env!("CARGO_PKG_DESCRIPTION"))
15 .subcommand(
16 Command::new("version")
17 .about("Prints version and build information")
18 )
19 .subcommand(
20 Command::new("config")
21 .about(" work with JACS configuration")
22 .subcommand(
23 Command::new("create")
24 .about(" create a config file")
25 )
26 .subcommand(
27 Command::new("read")
28 .about("read configuration and display to screen. This includes both the config file and the env variables.")
29 ),
30 )
31 .subcommand(
32 Command::new("agent")
33 .about(" work with a JACS agent")
34 .subcommand(
35 Command::new("dns")
36 .about("emit DNS TXT commands for publishing agent fingerprint")
37 .arg(
38 Arg::new("agent-file")
39 .short('a')
40 .long("agent-file")
41 .value_parser(value_parser!(String))
42 .help("Path to agent JSON (optional; defaults via config)"),
43 )
44 .arg(
45 Arg::new("no-dns")
46 .long("no-dns")
47 .help("Disable DNS validation; rely on embedded fingerprint")
48 .action(ArgAction::SetTrue),
49 )
50 .arg(
51 Arg::new("require-dns")
52 .long("require-dns")
53 .help("Require DNS validation; if domain missing, fail. Not strict (no DNSSEC required).")
54 .action(ArgAction::SetTrue),
55 )
56 .arg(
57 Arg::new("require-strict-dns")
58 .long("require-strict-dns")
59 .help("Require strict DNSSEC validation; if domain missing, fail.")
60 .action(ArgAction::SetTrue),
61 )
62 .arg(
63 Arg::new("ignore-dns")
64 .long("ignore-dns")
65 .help("Ignore DNS validation entirely.")
66 .action(ArgAction::SetTrue),
67 )
68 .arg(Arg::new("domain").long("domain").value_parser(value_parser!(String)))
69 .arg(Arg::new("agent-id").long("agent-id").value_parser(value_parser!(String)))
70 .arg(Arg::new("ttl").long("ttl").value_parser(value_parser!(u32)).default_value("3600"))
71 .arg(Arg::new("encoding").long("encoding").value_parser(["base64","hex"]).default_value("base64"))
72 .arg(Arg::new("provider").long("provider").value_parser(["plain","aws","azure","cloudflare"]).default_value("plain"))
73 )
74 .subcommand(
75 Command::new("create")
76 .about(" create an agent")
77 .arg(
78 Arg::new("filename")
79 .short('f')
80 .help("Name of the json file with agent schema and jacsAgentType")
81 .value_parser(value_parser!(String)),
82 )
83 .arg(
84 Arg::new("create-keys")
85 .long("create-keys")
86 .required(true)
87 .help("Create keys or not if they already exist. Configure key type in jacs.config.json")
88 .value_parser(value_parser!(bool)),
89 ),
90 )
91 .subcommand(
92 Command::new("verify")
93 .about(" verify an agent")
94 .arg(
95 Arg::new("agent-file")
96 .short('a')
97 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
98 .value_parser(value_parser!(String)),
99 )
100 .arg(
101 Arg::new("no-dns")
102 .long("no-dns")
103 .help("Disable DNS validation; rely on embedded fingerprint")
104 .action(ArgAction::SetTrue),
105 )
106 .arg(
107 Arg::new("require-dns")
108 .long("require-dns")
109 .help("Require DNS validation; if domain missing, fail. Not strict (no DNSSEC required).")
110 .action(ArgAction::SetTrue),
111 )
112 .arg(
113 Arg::new("require-strict-dns")
114 .long("require-strict-dns")
115 .help("Require strict DNSSEC validation; if domain missing, fail.")
116 .action(ArgAction::SetTrue),
117 )
118 .arg(
119 Arg::new("ignore-dns")
120 .long("ignore-dns")
121 .help("Ignore DNS validation entirely.")
122 .action(ArgAction::SetTrue),
123 ),
124 )
125 .subcommand(
126 Command::new("lookup")
127 .about("Look up another agent's public key and DNS info from their domain")
128 .arg(
129 Arg::new("domain")
130 .required(true)
131 .help("Domain to look up (e.g., agent.example.com)"),
132 )
133 .arg(
134 Arg::new("no-dns")
135 .long("no-dns")
136 .help("Skip DNS TXT record lookup")
137 .action(ArgAction::SetTrue),
138 )
139 .arg(
140 Arg::new("strict")
141 .long("strict")
142 .help("Require DNSSEC validation for DNS lookup")
143 .action(ArgAction::SetTrue),
144 ),
145 )
146 .subcommand(
147 Command::new("rotate-keys")
148 .about("Rotate the agent's cryptographic keys")
149 .arg(
150 Arg::new("algorithm")
151 .long("algorithm")
152 .value_parser(["ring-Ed25519", "pq2025"])
153 .help("Signing algorithm for the new keys (defaults to current)"),
154 )
155 .arg(
156 Arg::new("config")
157 .long("config")
158 .value_parser(value_parser!(String))
159 .help("Path to jacs.config.json (default: ./jacs.config.json)"),
160 ),
161 )
162 .subcommand(
163 Command::new("keys-list")
164 .about("List active and archived key files")
165 .arg(
166 Arg::new("config")
167 .long("config")
168 .value_parser(value_parser!(String))
169 .help("Path to jacs.config.json (default: ./jacs.config.json)"),
170 ),
171 )
172 .subcommand(
173 Command::new("repair")
174 .about("Repair config after an interrupted key rotation")
175 .arg(
176 Arg::new("config")
177 .long("config")
178 .value_parser(value_parser!(String))
179 .help("Path to jacs.config.json (default: ./jacs.config.json)"),
180 ),
181 ),
182 )
183
184 .subcommand(
185 Command::new("document")
186 .about(" work with a general JACS document")
187 .subcommand(
188 Command::new("create")
189 .about(" create a new JACS file, either by embedding or parsing a document")
190 .arg(
191 Arg::new("agent-file")
192 .short('a')
193 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
194 .value_parser(value_parser!(String)),
195 )
196 .arg(
197 Arg::new("filename")
198 .short('f')
199 .help("Path to input file. Must be JSON")
200 .value_parser(value_parser!(String)),
201 )
202 .arg(
203 Arg::new("output")
204 .short('o')
205 .help("Output filename. ")
206 .value_parser(value_parser!(String)),
207 )
208 .arg(
209 Arg::new("directory")
210 .short('d')
211 .help("Path to directory of files. Files should end with .json")
212 .value_parser(value_parser!(String)),
213 )
214 .arg(
215 Arg::new("verbose")
216 .short('v')
217 .long("verbose")
218 .action(ArgAction::SetTrue),
219 )
220 .arg(
221 Arg::new("no-save")
222 .long("no-save")
223 .short('n')
224 .help("Instead of saving files, print to stdout")
225 .action(ArgAction::SetTrue),
226 )
227 .arg(
228 Arg::new("schema")
229 .short('s')
230 .help("Path to JSON schema file to use to create")
231 .long("schema")
232 .value_parser(value_parser!(String)),
233 )
234 .arg(
235 Arg::new("attach")
236 .help("Path to file or directory for file attachments")
237 .long("attach")
238 .value_parser(value_parser!(String)),
239 )
240 .arg(
241 Arg::new("embed")
242 .short('e')
243 .help("Embed documents or keep the documents external")
244 .long("embed")
245 .value_parser(value_parser!(bool)),
246 ),
247 )
248 .subcommand(
249 Command::new("update")
250 .about("create a new version of document. requires both the original JACS file and the modified jacs metadata")
251 .arg(
252 Arg::new("agent-file")
253 .short('a')
254 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
255 .value_parser(value_parser!(String)),
256 )
257 .arg(
258 Arg::new("new")
259 .short('n')
260 .required(true)
261 .help("Path to new version of document.")
262 .value_parser(value_parser!(String)),
263 )
264 .arg(
265 Arg::new("filename")
266 .short('f')
267 .required(true)
268 .help("Path to original document.")
269 .value_parser(value_parser!(String)),
270 )
271 .arg(
272 Arg::new("output")
273 .short('o')
274 .help("Output filename. Filenames will always end with \"json\"")
275 .value_parser(value_parser!(String)),
276 )
277 .arg(
278 Arg::new("verbose")
279 .short('v')
280 .long("verbose")
281 .action(ArgAction::SetTrue),
282 )
283 .arg(
284 Arg::new("no-save")
285 .long("no-save")
286 .short('n')
287 .help("Instead of saving files, print to stdout")
288 .action(ArgAction::SetTrue),
289 )
290 .arg(
291 Arg::new("schema")
292 .short('s')
293 .help("Path to JSON schema file to use to create")
294 .long("schema")
295 .value_parser(value_parser!(String)),
296 )
297 .arg(
298 Arg::new("attach")
299 .help("Path to file or directory for file attachments")
300 .long("attach")
301 .value_parser(value_parser!(String)),
302 )
303 .arg(
304 Arg::new("embed")
305 .short('e')
306 .help("Embed documents or keep the documents external")
307 .long("embed")
308 .value_parser(value_parser!(bool)),
309 )
310 ,
311 )
312 .subcommand(
313 Command::new("check-agreement")
314 .about("given a document, provide alist of agents that should sign document")
315 .arg(
316 Arg::new("agent-file")
317 .short('a')
318 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
319 .value_parser(value_parser!(String)),
320 )
321 .arg(
322 Arg::new("filename")
323 .short('f')
324 .required(true)
325 .help("Path to original document.")
326 .value_parser(value_parser!(String)),
327 )
328 .arg(
329 Arg::new("directory")
330 .short('d')
331 .help("Path to directory of files. Files should end with .json")
332 .value_parser(value_parser!(String)),
333 )
334 .arg(
335 Arg::new("schema")
336 .short('s')
337 .help("Path to JSON schema file to use to create")
338 .long("schema")
339 .value_parser(value_parser!(String)),
340 )
341
342 )
343 .subcommand(
344 Command::new("create-agreement")
345 .about("given a document, provide alist of agents that should sign document")
346 .arg(
347 Arg::new("agent-file")
348 .short('a')
349 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
350 .value_parser(value_parser!(String)),
351 )
352 .arg(
353 Arg::new("filename")
354 .short('f')
355 .required(true)
356 .help("Path to original document.")
357 .value_parser(value_parser!(String)),
358 )
359 .arg(
360 Arg::new("directory")
361 .short('d')
362 .help("Path to directory of files. Files should end with .json")
363 .value_parser(value_parser!(String)),
364 )
365 .arg(
366 Arg::new("agentids")
367 .short('i')
368 .long("agentids")
369 .value_name("VALUES")
370 .help("Comma-separated list of agent ids")
371 .value_delimiter(',')
372 .required(true)
373 .action(clap::ArgAction::Set),
374 )
375 .arg(
376 Arg::new("output")
377 .short('o')
378 .help("Output filename. Filenames will always end with \"json\"")
379 .value_parser(value_parser!(String)),
380 )
381 .arg(
382 Arg::new("verbose")
383 .short('v')
384 .long("verbose")
385 .action(ArgAction::SetTrue),
386 )
387 .arg(
388 Arg::new("no-save")
389 .long("no-save")
390 .short('n')
391 .help("Instead of saving files, print to stdout")
392 .action(ArgAction::SetTrue),
393 )
394 .arg(
395 Arg::new("schema")
396 .short('s')
397 .help("Path to JSON schema file to use to create")
398 .long("schema")
399 .value_parser(value_parser!(String)),
400 )
401
402 ).subcommand(
403 Command::new("sign-agreement")
404 .about("given a document, sign the agreement section")
405 .arg(
406 Arg::new("agent-file")
407 .short('a')
408 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
409 .value_parser(value_parser!(String)),
410 )
411 .arg(
412 Arg::new("filename")
413 .short('f')
414 .required(true)
415 .help("Path to original document.")
416 .value_parser(value_parser!(String)),
417 )
418 .arg(
419 Arg::new("directory")
420 .short('d')
421 .help("Path to directory of files. Files should end with .json")
422 .value_parser(value_parser!(String)),
423 )
424 .arg(
425 Arg::new("output")
426 .short('o')
427 .help("Output filename. Filenames will always end with \"json\"")
428 .value_parser(value_parser!(String)),
429 )
430 .arg(
431 Arg::new("verbose")
432 .short('v')
433 .long("verbose")
434 .action(ArgAction::SetTrue),
435 )
436 .arg(
437 Arg::new("no-save")
438 .long("no-save")
439 .short('n')
440 .help("Instead of saving files, print to stdout")
441 .action(ArgAction::SetTrue),
442 )
443 .arg(
444 Arg::new("schema")
445 .short('s')
446 .help("Path to JSON schema file to use to create")
447 .long("schema")
448 .value_parser(value_parser!(String)),
449 )
450
451 )
452 .subcommand(
453 Command::new("verify")
454 .about(" verify a documents hash, siginatures, and schema")
455 .arg(
456 Arg::new("agent-file")
457 .short('a')
458 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
459 .value_parser(value_parser!(String)),
460 )
461 .arg(
462 Arg::new("filename")
463 .short('f')
464 .help("Path to input file. Must be JSON")
465 .value_parser(value_parser!(String)),
466 )
467 .arg(
468 Arg::new("directory")
469 .short('d')
470 .help("Path to directory of files. Files should end with .json")
471 .value_parser(value_parser!(String)),
472 )
473 .arg(
474 Arg::new("verbose")
475 .short('v')
476 .long("verbose")
477 .action(ArgAction::SetTrue),
478 )
479 .arg(
480 Arg::new("schema")
481 .short('s')
482 .help("Path to JSON schema file to use to validate")
483 .long("schema")
484 .value_parser(value_parser!(String)),
485 ),
486 )
487 .subcommand(
488 Command::new("extract")
489 .about(" given documents, extract embedded contents if any")
490 .arg(
491 Arg::new("agent-file")
492 .short('a')
493 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
494 .value_parser(value_parser!(String)),
495 )
496 .arg(
497 Arg::new("filename")
498 .short('f')
499 .help("Path to input file. Must be JSON")
500 .value_parser(value_parser!(String)),
501 )
502 .arg(
503 Arg::new("directory")
504 .short('d')
505 .help("Path to directory of files. Files should end with .json")
506 .value_parser(value_parser!(String)),
507 )
508 .arg(
509 Arg::new("verbose")
510 .short('v')
511 .long("verbose")
512 .action(ArgAction::SetTrue),
513 )
514 .arg(
515 Arg::new("schema")
516 .short('s')
517 .help("Path to JSON schema file to use to validate")
518 .long("schema")
519 .value_parser(value_parser!(String)),
520 ),
521 )
522 )
523 .subcommand(
524 Command::new("key")
525 .about("Work with JACS cryptographic keys")
526 .subcommand(
527 Command::new("reencrypt")
528 .about("Re-encrypt the private key with a new password")
529 )
530 )
531 .subcommand(
532 Command::new("mcp")
533 .about("Start the built-in JACS MCP server (stdio transport)")
534 .arg(
535 Arg::new("profile")
536 .long("profile")
537 .default_value("core")
538 .help("Tool profile: 'core' (default, core tools) or 'full' (all tools)"),
539 )
540 .subcommand(
541 Command::new("install")
542 .about("Deprecated: MCP is now built into the jacs binary")
543 .hide(true)
544 )
545 .subcommand(
546 Command::new("run")
547 .about("Deprecated: use `jacs mcp` directly")
548 .hide(true)
549 ),
550 )
551 .subcommand(
552 Command::new("a2a")
553 .about("A2A (Agent-to-Agent) trust and discovery commands")
554 .subcommand(
555 Command::new("assess")
556 .about("Assess trust level of a remote A2A Agent Card")
557 .arg(
558 Arg::new("source")
559 .required(true)
560 .help("Path to Agent Card JSON file or URL"),
561 )
562 .arg(
563 Arg::new("policy")
564 .long("policy")
565 .short('p')
566 .value_parser(["open", "verified", "strict"])
567 .default_value("verified")
568 .help("Trust policy to apply (default: verified)"),
569 )
570 .arg(
571 Arg::new("json")
572 .long("json")
573 .action(ArgAction::SetTrue)
574 .help("Output result as JSON"),
575 ),
576 )
577 .subcommand(
578 Command::new("trust")
579 .about("Add a remote A2A agent to the local trust store")
580 .arg(
581 Arg::new("source")
582 .required(true)
583 .help("Path to Agent Card JSON file or URL"),
584 ),
585 )
586 .subcommand(
587 Command::new("discover")
588 .about("Discover a remote A2A agent via its well-known Agent Card")
589 .arg(
590 Arg::new("url")
591 .required(true)
592 .help("Base URL of the agent (e.g. https://agent.example.com)"),
593 )
594 .arg(
595 Arg::new("json")
596 .long("json")
597 .action(ArgAction::SetTrue)
598 .help("Output the full Agent Card as JSON"),
599 )
600 .arg(
601 Arg::new("policy")
602 .long("policy")
603 .short('p')
604 .value_parser(["open", "verified", "strict"])
605 .default_value("verified")
606 .help("Trust policy to apply against the discovered card"),
607 ),
608 )
609 .subcommand(
610 Command::new("serve")
611 .about("Serve this agent's .well-known endpoints for A2A discovery")
612 .arg(
613 Arg::new("port")
614 .long("port")
615 .value_parser(value_parser!(u16))
616 .default_value("8080")
617 .help("Port to listen on (default: 8080)"),
618 )
619 .arg(
620 Arg::new("host")
621 .long("host")
622 .default_value("127.0.0.1")
623 .help("Host to bind to (default: 127.0.0.1)"),
624 ),
625 )
626 .subcommand(
627 Command::new("quickstart")
628 .about("Create/load an agent and start serving A2A endpoints (password required)")
629 .after_help(quickstart_password_bootstrap_help())
630 .arg(
631 Arg::new("name")
632 .long("name")
633 .value_parser(value_parser!(String))
634 .required(true)
635 .help("Agent name used for first-time quickstart creation"),
636 )
637 .arg(
638 Arg::new("domain")
639 .long("domain")
640 .value_parser(value_parser!(String))
641 .required(true)
642 .help("Agent domain used for DNS/public-key verification workflows"),
643 )
644 .arg(
645 Arg::new("description")
646 .long("description")
647 .value_parser(value_parser!(String))
648 .help("Optional human-readable agent description"),
649 )
650 .arg(
651 Arg::new("port")
652 .long("port")
653 .value_parser(value_parser!(u16))
654 .default_value("8080")
655 .help("Port to listen on (default: 8080)"),
656 )
657 .arg(
658 Arg::new("host")
659 .long("host")
660 .default_value("127.0.0.1")
661 .help("Host to bind to (default: 127.0.0.1)"),
662 )
663 .arg(
664 Arg::new("algorithm")
665 .long("algorithm")
666 .short('a')
667 .value_parser(["pq2025", "ring-Ed25519"])
668 .help("Signing algorithm (default: pq2025)"),
669 ),
670 ),
671 )
672 .subcommand(
673 Command::new("quickstart")
674 .about("Create or load a persistent agent for instant sign/verify (password required)")
675 .after_help(quickstart_password_bootstrap_help())
676 .arg(
677 Arg::new("name")
678 .long("name")
679 .value_parser(value_parser!(String))
680 .required(true)
681 .help("Agent name used for first-time quickstart creation"),
682 )
683 .arg(
684 Arg::new("domain")
685 .long("domain")
686 .value_parser(value_parser!(String))
687 .required(true)
688 .help("Agent domain used for DNS/public-key verification workflows"),
689 )
690 .arg(
691 Arg::new("description")
692 .long("description")
693 .value_parser(value_parser!(String))
694 .help("Optional human-readable agent description"),
695 )
696 .arg(
697 Arg::new("algorithm")
698 .long("algorithm")
699 .short('a')
700 .value_parser(["ed25519", "pq2025"])
701 .default_value("pq2025")
702 .help("Signing algorithm (default: pq2025)"),
703 )
704 .arg(
705 Arg::new("sign")
706 .long("sign")
707 .help("Sign JSON from stdin and print signed document to stdout")
708 .action(ArgAction::SetTrue),
709 )
710 .arg(
711 Arg::new("file")
712 .short('f')
713 .long("file")
714 .value_parser(value_parser!(String))
715 .help("Sign a JSON file instead of reading from stdin (used with --sign)"),
716 )
717 )
718 .subcommand(
719 Command::new("init")
720 .about("Initialize JACS by creating both config and agent (with keys)")
721 .arg(
722 Arg::new("yes")
723 .long("yes")
724 .short('y')
725 .action(ArgAction::SetTrue)
726 .help("Automatically set the new agent ID in jacs.config.json without prompting"),
727 )
728 )
729 .subcommand(
730 Command::new("attest")
731 .about("Create and verify attestation documents")
732 .subcommand(
733 Command::new("create")
734 .about("Create a signed attestation")
735 .arg(
736 Arg::new("subject-type")
737 .long("subject-type")
738 .value_parser(["agent", "artifact", "workflow", "identity"])
739 .help("Type of subject being attested"),
740 )
741 .arg(
742 Arg::new("subject-id")
743 .long("subject-id")
744 .value_parser(value_parser!(String))
745 .help("Identifier of the subject"),
746 )
747 .arg(
748 Arg::new("subject-digest")
749 .long("subject-digest")
750 .value_parser(value_parser!(String))
751 .help("SHA-256 digest of the subject"),
752 )
753 .arg(
754 Arg::new("claims")
755 .long("claims")
756 .value_parser(value_parser!(String))
757 .required(true)
758 .help("JSON array of claims, e.g. '[{\"name\":\"reviewed\",\"value\":true}]'"),
759 )
760 .arg(
761 Arg::new("evidence")
762 .long("evidence")
763 .value_parser(value_parser!(String))
764 .help("JSON array of evidence references"),
765 )
766 .arg(
767 Arg::new("from-document")
768 .long("from-document")
769 .value_parser(value_parser!(String))
770 .help("Lift attestation from an existing signed document file"),
771 )
772 .arg(
773 Arg::new("output")
774 .short('o')
775 .long("output")
776 .value_parser(value_parser!(String))
777 .help("Write attestation to file instead of stdout"),
778 ),
779 )
780 .subcommand(
781 Command::new("verify")
782 .about("Verify an attestation document")
783 .arg(
784 Arg::new("file")
785 .help("Path to the attestation JSON file")
786 .required(true)
787 .value_parser(value_parser!(String)),
788 )
789 .arg(
790 Arg::new("full")
791 .long("full")
792 .action(ArgAction::SetTrue)
793 .help("Use full verification (evidence + derivation chain)"),
794 )
795 .arg(
796 Arg::new("json")
797 .long("json")
798 .action(ArgAction::SetTrue)
799 .help("Output result as JSON"),
800 )
801 .arg(
802 Arg::new("key-dir")
803 .long("key-dir")
804 .value_parser(value_parser!(String))
805 .help("Directory containing public keys for verification"),
806 )
807 .arg(
808 Arg::new("max-depth")
809 .long("max-depth")
810 .value_parser(value_parser!(u32))
811 .help("Maximum derivation chain depth"),
812 ),
813 )
814 .subcommand(
815 Command::new("export-dsse")
816 .about("Export an attestation as a DSSE envelope for in-toto/SLSA")
817 .arg(
818 Arg::new("file")
819 .help("Path to the signed attestation JSON file")
820 .required(true)
821 .value_parser(value_parser!(String)),
822 )
823 .arg(
824 Arg::new("output")
825 .short('o')
826 .long("output")
827 .value_parser(value_parser!(String))
828 .help("Write DSSE envelope to file instead of stdout"),
829 ),
830 )
831 .subcommand_required(true)
832 .arg_required_else_help(true),
833 )
834 .subcommand(
835 Command::new("verify")
836 .about("Verify a signed JACS document (no agent required)")
837 .arg(
838 Arg::new("file")
839 .help("Path to the signed JACS JSON file")
840 .required_unless_present("remote")
841 .value_parser(value_parser!(String)),
842 )
843 .arg(
844 Arg::new("remote")
845 .long("remote")
846 .value_parser(value_parser!(String))
847 .help("Fetch document from URL before verifying"),
848 )
849 .arg(
850 Arg::new("json")
851 .long("json")
852 .action(ArgAction::SetTrue)
853 .help("Output result as JSON"),
854 )
855 .arg(
856 Arg::new("key-dir")
857 .long("key-dir")
858 .value_parser(value_parser!(String))
859 .help("Directory containing public keys for verification"),
860 )
861 );
862
863 #[cfg(feature = "keychain")]
865 let cmd = cmd.subcommand(
866 Command::new("keychain")
867 .about("Manage private key passwords in the OS keychain (per-agent)")
868 .subcommand(
869 Command::new("set")
870 .about("Store a password in the OS keychain for an agent")
871 .arg(
872 Arg::new("agent-id")
873 .long("agent-id")
874 .help("Agent ID to associate the password with")
875 .value_name("AGENT_ID")
876 .required(true),
877 )
878 .arg(
879 Arg::new("password")
880 .long("password")
881 .help("Password to store (if omitted, prompts interactively)")
882 .value_name("PASSWORD"),
883 ),
884 )
885 .subcommand(
886 Command::new("get")
887 .about("Retrieve the stored password for an agent (prints to stdout)")
888 .arg(
889 Arg::new("agent-id")
890 .long("agent-id")
891 .help("Agent ID to look up")
892 .value_name("AGENT_ID")
893 .required(true),
894 ),
895 )
896 .subcommand(
897 Command::new("delete")
898 .about("Remove the stored password for an agent from the OS keychain")
899 .arg(
900 Arg::new("agent-id")
901 .long("agent-id")
902 .help("Agent ID whose password to delete")
903 .value_name("AGENT_ID")
904 .required(true),
905 ),
906 )
907 .subcommand(
908 Command::new("status")
909 .about("Check if a password is stored for an agent in the OS keychain")
910 .arg(
911 Arg::new("agent-id")
912 .long("agent-id")
913 .help("Agent ID to check")
914 .value_name("AGENT_ID")
915 .required(true),
916 ),
917 )
918 .arg_required_else_help(true),
919 );
920
921 let cmd = cmd.subcommand(
922 Command::new("convert")
923 .about(
924 "Convert JACS documents between JSON, YAML, and HTML formats (no agent required)",
925 )
926 .arg(
927 Arg::new("to")
928 .long("to")
929 .required(true)
930 .value_parser(["json", "yaml", "html"])
931 .help("Target format: json, yaml, or html"),
932 )
933 .arg(
934 Arg::new("from")
935 .long("from")
936 .value_parser(["json", "yaml", "html"])
937 .help("Source format (auto-detected from extension if omitted)"),
938 )
939 .arg(
940 Arg::new("file")
941 .short('f')
942 .long("file")
943 .required(true)
944 .value_parser(value_parser!(String))
945 .help("Input file path (use '-' for stdin)"),
946 )
947 .arg(
948 Arg::new("output")
949 .short('o')
950 .long("output")
951 .value_parser(value_parser!(String))
952 .help("Output file path (defaults to stdout)"),
953 ),
954 );
955
956 cmd.subcommand(
959 Command::new("sign-text")
960 .about("Sign a text/markdown file in place with an inline JACS signature")
961 .arg(
962 Arg::new("file")
963 .help("Path to the text file to sign in place")
964 .required(true)
965 .value_parser(value_parser!(String)),
966 )
967 .arg(
968 Arg::new("no-backup")
969 .long("no-backup")
970 .action(ArgAction::SetTrue)
971 .help("Skip the automatic <path>.bak backup"),
972 )
973 .arg(
974 Arg::new("json")
975 .long("json")
976 .action(ArgAction::SetTrue)
977 .help("Output result as JSON"),
978 ),
979 )
980 .subcommand(
981 Command::new("verify-text")
982 .about("Verify inline JACS signatures in a text/markdown file")
983 .arg(
984 Arg::new("file")
985 .help("Path to the signed text file")
986 .required(true)
987 .value_parser(value_parser!(String)),
988 )
989 .arg(
990 Arg::new("key-dir")
991 .long("key-dir")
992 .value_parser(value_parser!(String))
993 .help("Directory containing signer public keys (.public.pem)"),
994 )
995 .arg(
996 Arg::new("json")
997 .long("json")
998 .action(ArgAction::SetTrue)
999 .help("Output result as JSON"),
1000 )
1001 .arg(
1002 Arg::new("strict")
1003 .long("strict")
1004 .action(ArgAction::SetTrue)
1005 .help(
1006 "Treat 'no JACS signature found' as a hard failure (exits 1 instead of 2)",
1007 ),
1008 ),
1009 )
1010 .subcommand(
1011 Command::new("sign-image")
1012 .about("Sign an image (PNG, JPEG, WebP) by embedding a JACS signature")
1013 .arg(
1014 Arg::new("input")
1015 .help("Path to the input image")
1016 .required(true)
1017 .value_parser(value_parser!(String)),
1018 )
1019 .arg(
1020 Arg::new("out")
1021 .long("out")
1022 .required(true)
1023 .value_parser(value_parser!(String))
1024 .help("Output image path"),
1025 )
1026 .arg(
1027 Arg::new("robust")
1028 .long("robust")
1029 .action(ArgAction::SetTrue)
1030 .help("Enable LSB fallback encoding (modifies pixel data; PNG/JPEG only)"),
1031 )
1032 .arg(
1033 Arg::new("format")
1034 .long("format")
1035 .value_parser(["png", "jpeg", "webp"])
1036 .help("Force a specific format (auto-detected by default)"),
1037 )
1038 .arg(
1039 Arg::new("refuse-overwrite")
1040 .long("refuse-overwrite")
1041 .action(ArgAction::SetTrue)
1042 .help("Refuse to overwrite an existing JACS signature on the input"),
1043 )
1044 .arg(
1045 Arg::new("json")
1046 .long("json")
1047 .action(ArgAction::SetTrue)
1048 .help("Output result as JSON"),
1049 ),
1050 )
1051 .subcommand(
1052 Command::new("verify-image")
1053 .about("Verify an embedded JACS signature in an image")
1054 .arg(
1055 Arg::new("file")
1056 .help("Path to the signed image")
1057 .required(true)
1058 .value_parser(value_parser!(String)),
1059 )
1060 .arg(
1061 Arg::new("key-dir")
1062 .long("key-dir")
1063 .value_parser(value_parser!(String))
1064 .help("Directory containing signer public keys (.public.pem)"),
1065 )
1066 .arg(
1067 Arg::new("json")
1068 .long("json")
1069 .action(ArgAction::SetTrue)
1070 .help("Output result as JSON"),
1071 )
1072 .arg(
1073 Arg::new("strict")
1074 .long("strict")
1075 .action(ArgAction::SetTrue)
1076 .help(
1077 "Treat 'no JACS signature found' as a hard failure (exits 1 instead of 2)",
1078 ),
1079 )
1080 .arg(
1081 Arg::new("robust")
1082 .long("robust")
1083 .action(ArgAction::SetTrue)
1084 .help("Scan LSB channel for the robust-mode payload (default off)"),
1085 ),
1086 )
1087 .subcommand(
1088 Command::new("extract-media-signature")
1089 .about("Extract the embedded JACS signature payload from an image")
1090 .arg(
1091 Arg::new("file")
1092 .help("Path to the image to extract from")
1093 .required(true)
1094 .value_parser(value_parser!(String)),
1095 )
1096 .arg(
1097 Arg::new("raw-payload")
1098 .long("raw-payload")
1099 .action(ArgAction::SetTrue)
1100 .help("Print the raw base64url wire form instead of the decoded JSON"),
1101 )
1102 .arg(
1103 Arg::new("robust")
1104 .long("robust")
1105 .action(ArgAction::SetTrue)
1106 .help(
1107 "Scan the LSB channel as a fallback if the metadata channel has \
1108 no payload (R-011; mirrors verify-image --robust)",
1109 ),
1110 ),
1111 )
1112}