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("task")
186 .about(" work with a JACS Agent task")
187 .subcommand(
188 Command::new("create")
189 .about(" create a new JACS Task 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("name")
204 .short('n')
205 .required(true)
206 .help("name of task")
207 .value_parser(value_parser!(String)),
208 )
209 .arg(
210 Arg::new("description")
211 .short('d')
212 .required(true)
213 .help("description of task")
214 .value_parser(value_parser!(String)),
215 )
216 )
217 .subcommand(
218 Command::new("update")
219 .about("update an existing task document")
220 .arg(
221 Arg::new("filename")
222 .short('f')
223 .required(true)
224 .help("Path to the updated task JSON file")
225 .value_parser(value_parser!(String)),
226 )
227 .arg(
228 Arg::new("task-key")
229 .short('k')
230 .required(true)
231 .help("Task document key (id:version)")
232 .value_parser(value_parser!(String)),
233 )
234 )
235 )
236
237 .subcommand(
238 Command::new("document")
239 .about(" work with a general JACS document")
240 .subcommand(
241 Command::new("create")
242 .about(" create a new JACS file, either by embedding or parsing a document")
243 .arg(
244 Arg::new("agent-file")
245 .short('a')
246 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
247 .value_parser(value_parser!(String)),
248 )
249 .arg(
250 Arg::new("filename")
251 .short('f')
252 .help("Path to input file. Must be JSON")
253 .value_parser(value_parser!(String)),
254 )
255 .arg(
256 Arg::new("output")
257 .short('o')
258 .help("Output filename. ")
259 .value_parser(value_parser!(String)),
260 )
261 .arg(
262 Arg::new("directory")
263 .short('d')
264 .help("Path to directory of files. Files should end with .json")
265 .value_parser(value_parser!(String)),
266 )
267 .arg(
268 Arg::new("verbose")
269 .short('v')
270 .long("verbose")
271 .action(ArgAction::SetTrue),
272 )
273 .arg(
274 Arg::new("no-save")
275 .long("no-save")
276 .short('n')
277 .help("Instead of saving files, print to stdout")
278 .action(ArgAction::SetTrue),
279 )
280 .arg(
281 Arg::new("schema")
282 .short('s')
283 .help("Path to JSON schema file to use to create")
284 .long("schema")
285 .value_parser(value_parser!(String)),
286 )
287 .arg(
288 Arg::new("attach")
289 .help("Path to file or directory for file attachments")
290 .long("attach")
291 .value_parser(value_parser!(String)),
292 )
293 .arg(
294 Arg::new("embed")
295 .short('e')
296 .help("Embed documents or keep the documents external")
297 .long("embed")
298 .value_parser(value_parser!(bool)),
299 ),
300 )
301 .subcommand(
302 Command::new("update")
303 .about("create a new version of document. requires both the original JACS file and the modified jacs metadata")
304 .arg(
305 Arg::new("agent-file")
306 .short('a')
307 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
308 .value_parser(value_parser!(String)),
309 )
310 .arg(
311 Arg::new("new")
312 .short('n')
313 .required(true)
314 .help("Path to new version of document.")
315 .value_parser(value_parser!(String)),
316 )
317 .arg(
318 Arg::new("filename")
319 .short('f')
320 .required(true)
321 .help("Path to original document.")
322 .value_parser(value_parser!(String)),
323 )
324 .arg(
325 Arg::new("output")
326 .short('o')
327 .help("Output filename. Filenames will always end with \"json\"")
328 .value_parser(value_parser!(String)),
329 )
330 .arg(
331 Arg::new("verbose")
332 .short('v')
333 .long("verbose")
334 .action(ArgAction::SetTrue),
335 )
336 .arg(
337 Arg::new("no-save")
338 .long("no-save")
339 .short('n')
340 .help("Instead of saving files, print to stdout")
341 .action(ArgAction::SetTrue),
342 )
343 .arg(
344 Arg::new("schema")
345 .short('s')
346 .help("Path to JSON schema file to use to create")
347 .long("schema")
348 .value_parser(value_parser!(String)),
349 )
350 .arg(
351 Arg::new("attach")
352 .help("Path to file or directory for file attachments")
353 .long("attach")
354 .value_parser(value_parser!(String)),
355 )
356 .arg(
357 Arg::new("embed")
358 .short('e')
359 .help("Embed documents or keep the documents external")
360 .long("embed")
361 .value_parser(value_parser!(bool)),
362 )
363 ,
364 )
365 .subcommand(
366 Command::new("check-agreement")
367 .about("given a document, provide alist of agents that should sign document")
368 .arg(
369 Arg::new("agent-file")
370 .short('a')
371 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
372 .value_parser(value_parser!(String)),
373 )
374 .arg(
375 Arg::new("filename")
376 .short('f')
377 .required(true)
378 .help("Path to original document.")
379 .value_parser(value_parser!(String)),
380 )
381 .arg(
382 Arg::new("directory")
383 .short('d')
384 .help("Path to directory of files. Files should end with .json")
385 .value_parser(value_parser!(String)),
386 )
387 .arg(
388 Arg::new("schema")
389 .short('s')
390 .help("Path to JSON schema file to use to create")
391 .long("schema")
392 .value_parser(value_parser!(String)),
393 )
394
395 )
396 .subcommand(
397 Command::new("create-agreement")
398 .about("given a document, provide alist of agents that should sign document")
399 .arg(
400 Arg::new("agent-file")
401 .short('a')
402 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
403 .value_parser(value_parser!(String)),
404 )
405 .arg(
406 Arg::new("filename")
407 .short('f')
408 .required(true)
409 .help("Path to original document.")
410 .value_parser(value_parser!(String)),
411 )
412 .arg(
413 Arg::new("directory")
414 .short('d')
415 .help("Path to directory of files. Files should end with .json")
416 .value_parser(value_parser!(String)),
417 )
418 .arg(
419 Arg::new("agentids")
420 .short('i')
421 .long("agentids")
422 .value_name("VALUES")
423 .help("Comma-separated list of agent ids")
424 .value_delimiter(',')
425 .required(true)
426 .action(clap::ArgAction::Set),
427 )
428 .arg(
429 Arg::new("output")
430 .short('o')
431 .help("Output filename. Filenames will always end with \"json\"")
432 .value_parser(value_parser!(String)),
433 )
434 .arg(
435 Arg::new("verbose")
436 .short('v')
437 .long("verbose")
438 .action(ArgAction::SetTrue),
439 )
440 .arg(
441 Arg::new("no-save")
442 .long("no-save")
443 .short('n')
444 .help("Instead of saving files, print to stdout")
445 .action(ArgAction::SetTrue),
446 )
447 .arg(
448 Arg::new("schema")
449 .short('s')
450 .help("Path to JSON schema file to use to create")
451 .long("schema")
452 .value_parser(value_parser!(String)),
453 )
454
455 ).subcommand(
456 Command::new("sign-agreement")
457 .about("given a document, sign the agreement section")
458 .arg(
459 Arg::new("agent-file")
460 .short('a')
461 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
462 .value_parser(value_parser!(String)),
463 )
464 .arg(
465 Arg::new("filename")
466 .short('f')
467 .required(true)
468 .help("Path to original document.")
469 .value_parser(value_parser!(String)),
470 )
471 .arg(
472 Arg::new("directory")
473 .short('d')
474 .help("Path to directory of files. Files should end with .json")
475 .value_parser(value_parser!(String)),
476 )
477 .arg(
478 Arg::new("output")
479 .short('o')
480 .help("Output filename. Filenames will always end with \"json\"")
481 .value_parser(value_parser!(String)),
482 )
483 .arg(
484 Arg::new("verbose")
485 .short('v')
486 .long("verbose")
487 .action(ArgAction::SetTrue),
488 )
489 .arg(
490 Arg::new("no-save")
491 .long("no-save")
492 .short('n')
493 .help("Instead of saving files, print to stdout")
494 .action(ArgAction::SetTrue),
495 )
496 .arg(
497 Arg::new("schema")
498 .short('s')
499 .help("Path to JSON schema file to use to create")
500 .long("schema")
501 .value_parser(value_parser!(String)),
502 )
503
504 )
505 .subcommand(
506 Command::new("verify")
507 .about(" verify a documents hash, siginatures, and schema")
508 .arg(
509 Arg::new("agent-file")
510 .short('a')
511 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
512 .value_parser(value_parser!(String)),
513 )
514 .arg(
515 Arg::new("filename")
516 .short('f')
517 .help("Path to input file. Must be JSON")
518 .value_parser(value_parser!(String)),
519 )
520 .arg(
521 Arg::new("directory")
522 .short('d')
523 .help("Path to directory of files. Files should end with .json")
524 .value_parser(value_parser!(String)),
525 )
526 .arg(
527 Arg::new("verbose")
528 .short('v')
529 .long("verbose")
530 .action(ArgAction::SetTrue),
531 )
532 .arg(
533 Arg::new("schema")
534 .short('s')
535 .help("Path to JSON schema file to use to validate")
536 .long("schema")
537 .value_parser(value_parser!(String)),
538 ),
539 )
540 .subcommand(
541 Command::new("extract")
542 .about(" given documents, extract embedded contents if any")
543 .arg(
544 Arg::new("agent-file")
545 .short('a')
546 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
547 .value_parser(value_parser!(String)),
548 )
549 .arg(
550 Arg::new("filename")
551 .short('f')
552 .help("Path to input file. Must be JSON")
553 .value_parser(value_parser!(String)),
554 )
555 .arg(
556 Arg::new("directory")
557 .short('d')
558 .help("Path to directory of files. Files should end with .json")
559 .value_parser(value_parser!(String)),
560 )
561 .arg(
562 Arg::new("verbose")
563 .short('v')
564 .long("verbose")
565 .action(ArgAction::SetTrue),
566 )
567 .arg(
568 Arg::new("schema")
569 .short('s')
570 .help("Path to JSON schema file to use to validate")
571 .long("schema")
572 .value_parser(value_parser!(String)),
573 ),
574 )
575 )
576 .subcommand(
577 Command::new("key")
578 .about("Work with JACS cryptographic keys")
579 .subcommand(
580 Command::new("reencrypt")
581 .about("Re-encrypt the private key with a new password")
582 )
583 )
584 .subcommand(
585 Command::new("mcp")
586 .about("Start the built-in JACS MCP server (stdio transport)")
587 .arg(
588 Arg::new("profile")
589 .long("profile")
590 .default_value("core")
591 .help("Tool profile: 'core' (default, core tools) or 'full' (all tools)"),
592 )
593 .subcommand(
594 Command::new("install")
595 .about("Deprecated: MCP is now built into the jacs binary")
596 .hide(true)
597 )
598 .subcommand(
599 Command::new("run")
600 .about("Deprecated: use `jacs mcp` directly")
601 .hide(true)
602 ),
603 )
604 .subcommand(
605 Command::new("a2a")
606 .about("A2A (Agent-to-Agent) trust and discovery commands")
607 .subcommand(
608 Command::new("assess")
609 .about("Assess trust level of a remote A2A Agent Card")
610 .arg(
611 Arg::new("source")
612 .required(true)
613 .help("Path to Agent Card JSON file or URL"),
614 )
615 .arg(
616 Arg::new("policy")
617 .long("policy")
618 .short('p')
619 .value_parser(["open", "verified", "strict"])
620 .default_value("verified")
621 .help("Trust policy to apply (default: verified)"),
622 )
623 .arg(
624 Arg::new("json")
625 .long("json")
626 .action(ArgAction::SetTrue)
627 .help("Output result as JSON"),
628 ),
629 )
630 .subcommand(
631 Command::new("trust")
632 .about("Add a remote A2A agent to the local trust store")
633 .arg(
634 Arg::new("source")
635 .required(true)
636 .help("Path to Agent Card JSON file or URL"),
637 ),
638 )
639 .subcommand(
640 Command::new("discover")
641 .about("Discover a remote A2A agent via its well-known Agent Card")
642 .arg(
643 Arg::new("url")
644 .required(true)
645 .help("Base URL of the agent (e.g. https://agent.example.com)"),
646 )
647 .arg(
648 Arg::new("json")
649 .long("json")
650 .action(ArgAction::SetTrue)
651 .help("Output the full Agent Card as JSON"),
652 )
653 .arg(
654 Arg::new("policy")
655 .long("policy")
656 .short('p')
657 .value_parser(["open", "verified", "strict"])
658 .default_value("verified")
659 .help("Trust policy to apply against the discovered card"),
660 ),
661 )
662 .subcommand(
663 Command::new("serve")
664 .about("Serve this agent's .well-known endpoints for A2A discovery")
665 .arg(
666 Arg::new("port")
667 .long("port")
668 .value_parser(value_parser!(u16))
669 .default_value("8080")
670 .help("Port to listen on (default: 8080)"),
671 )
672 .arg(
673 Arg::new("host")
674 .long("host")
675 .default_value("127.0.0.1")
676 .help("Host to bind to (default: 127.0.0.1)"),
677 ),
678 )
679 .subcommand(
680 Command::new("quickstart")
681 .about("Create/load an agent and start serving A2A endpoints (password required)")
682 .after_help(quickstart_password_bootstrap_help())
683 .arg(
684 Arg::new("name")
685 .long("name")
686 .value_parser(value_parser!(String))
687 .required(true)
688 .help("Agent name used for first-time quickstart creation"),
689 )
690 .arg(
691 Arg::new("domain")
692 .long("domain")
693 .value_parser(value_parser!(String))
694 .required(true)
695 .help("Agent domain used for DNS/public-key verification workflows"),
696 )
697 .arg(
698 Arg::new("description")
699 .long("description")
700 .value_parser(value_parser!(String))
701 .help("Optional human-readable agent description"),
702 )
703 .arg(
704 Arg::new("port")
705 .long("port")
706 .value_parser(value_parser!(u16))
707 .default_value("8080")
708 .help("Port to listen on (default: 8080)"),
709 )
710 .arg(
711 Arg::new("host")
712 .long("host")
713 .default_value("127.0.0.1")
714 .help("Host to bind to (default: 127.0.0.1)"),
715 )
716 .arg(
717 Arg::new("algorithm")
718 .long("algorithm")
719 .short('a')
720 .value_parser(["pq2025", "ring-Ed25519"])
721 .help("Signing algorithm (default: pq2025)"),
722 ),
723 ),
724 )
725 .subcommand(
726 Command::new("quickstart")
727 .about("Create or load a persistent agent for instant sign/verify (password required)")
728 .after_help(quickstart_password_bootstrap_help())
729 .arg(
730 Arg::new("name")
731 .long("name")
732 .value_parser(value_parser!(String))
733 .required(true)
734 .help("Agent name used for first-time quickstart creation"),
735 )
736 .arg(
737 Arg::new("domain")
738 .long("domain")
739 .value_parser(value_parser!(String))
740 .required(true)
741 .help("Agent domain used for DNS/public-key verification workflows"),
742 )
743 .arg(
744 Arg::new("description")
745 .long("description")
746 .value_parser(value_parser!(String))
747 .help("Optional human-readable agent description"),
748 )
749 .arg(
750 Arg::new("algorithm")
751 .long("algorithm")
752 .short('a')
753 .value_parser(["ed25519", "pq2025"])
754 .default_value("pq2025")
755 .help("Signing algorithm (default: pq2025)"),
756 )
757 .arg(
758 Arg::new("sign")
759 .long("sign")
760 .help("Sign JSON from stdin and print signed document to stdout")
761 .action(ArgAction::SetTrue),
762 )
763 .arg(
764 Arg::new("file")
765 .short('f')
766 .long("file")
767 .value_parser(value_parser!(String))
768 .help("Sign a JSON file instead of reading from stdin (used with --sign)"),
769 )
770 )
771 .subcommand(
772 Command::new("init")
773 .about("Initialize JACS by creating both config and agent (with keys)")
774 .arg(
775 Arg::new("yes")
776 .long("yes")
777 .short('y')
778 .action(ArgAction::SetTrue)
779 .help("Automatically set the new agent ID in jacs.config.json without prompting"),
780 )
781 )
782 .subcommand(
783 Command::new("attest")
784 .about("Create and verify attestation documents")
785 .subcommand(
786 Command::new("create")
787 .about("Create a signed attestation")
788 .arg(
789 Arg::new("subject-type")
790 .long("subject-type")
791 .value_parser(["agent", "artifact", "workflow", "identity"])
792 .help("Type of subject being attested"),
793 )
794 .arg(
795 Arg::new("subject-id")
796 .long("subject-id")
797 .value_parser(value_parser!(String))
798 .help("Identifier of the subject"),
799 )
800 .arg(
801 Arg::new("subject-digest")
802 .long("subject-digest")
803 .value_parser(value_parser!(String))
804 .help("SHA-256 digest of the subject"),
805 )
806 .arg(
807 Arg::new("claims")
808 .long("claims")
809 .value_parser(value_parser!(String))
810 .required(true)
811 .help("JSON array of claims, e.g. '[{\"name\":\"reviewed\",\"value\":true}]'"),
812 )
813 .arg(
814 Arg::new("evidence")
815 .long("evidence")
816 .value_parser(value_parser!(String))
817 .help("JSON array of evidence references"),
818 )
819 .arg(
820 Arg::new("from-document")
821 .long("from-document")
822 .value_parser(value_parser!(String))
823 .help("Lift attestation from an existing signed document file"),
824 )
825 .arg(
826 Arg::new("output")
827 .short('o')
828 .long("output")
829 .value_parser(value_parser!(String))
830 .help("Write attestation to file instead of stdout"),
831 ),
832 )
833 .subcommand(
834 Command::new("verify")
835 .about("Verify an attestation document")
836 .arg(
837 Arg::new("file")
838 .help("Path to the attestation JSON file")
839 .required(true)
840 .value_parser(value_parser!(String)),
841 )
842 .arg(
843 Arg::new("full")
844 .long("full")
845 .action(ArgAction::SetTrue)
846 .help("Use full verification (evidence + derivation chain)"),
847 )
848 .arg(
849 Arg::new("json")
850 .long("json")
851 .action(ArgAction::SetTrue)
852 .help("Output result as JSON"),
853 )
854 .arg(
855 Arg::new("key-dir")
856 .long("key-dir")
857 .value_parser(value_parser!(String))
858 .help("Directory containing public keys for verification"),
859 )
860 .arg(
861 Arg::new("max-depth")
862 .long("max-depth")
863 .value_parser(value_parser!(u32))
864 .help("Maximum derivation chain depth"),
865 ),
866 )
867 .subcommand(
868 Command::new("export-dsse")
869 .about("Export an attestation as a DSSE envelope for in-toto/SLSA")
870 .arg(
871 Arg::new("file")
872 .help("Path to the signed attestation JSON file")
873 .required(true)
874 .value_parser(value_parser!(String)),
875 )
876 .arg(
877 Arg::new("output")
878 .short('o')
879 .long("output")
880 .value_parser(value_parser!(String))
881 .help("Write DSSE envelope to file instead of stdout"),
882 ),
883 )
884 .subcommand_required(true)
885 .arg_required_else_help(true),
886 )
887 .subcommand(
888 Command::new("verify")
889 .about("Verify a signed JACS document (no agent required)")
890 .arg(
891 Arg::new("file")
892 .help("Path to the signed JACS JSON file")
893 .required_unless_present("remote")
894 .value_parser(value_parser!(String)),
895 )
896 .arg(
897 Arg::new("remote")
898 .long("remote")
899 .value_parser(value_parser!(String))
900 .help("Fetch document from URL before verifying"),
901 )
902 .arg(
903 Arg::new("json")
904 .long("json")
905 .action(ArgAction::SetTrue)
906 .help("Output result as JSON"),
907 )
908 .arg(
909 Arg::new("key-dir")
910 .long("key-dir")
911 .value_parser(value_parser!(String))
912 .help("Directory containing public keys for verification"),
913 )
914 );
915
916 #[cfg(feature = "keychain")]
918 let cmd = cmd.subcommand(
919 Command::new("keychain")
920 .about("Manage private key passwords in the OS keychain (per-agent)")
921 .subcommand(
922 Command::new("set")
923 .about("Store a password in the OS keychain for an agent")
924 .arg(
925 Arg::new("agent-id")
926 .long("agent-id")
927 .help("Agent ID to associate the password with")
928 .value_name("AGENT_ID")
929 .required(true),
930 )
931 .arg(
932 Arg::new("password")
933 .long("password")
934 .help("Password to store (if omitted, prompts interactively)")
935 .value_name("PASSWORD"),
936 ),
937 )
938 .subcommand(
939 Command::new("get")
940 .about("Retrieve the stored password for an agent (prints to stdout)")
941 .arg(
942 Arg::new("agent-id")
943 .long("agent-id")
944 .help("Agent ID to look up")
945 .value_name("AGENT_ID")
946 .required(true),
947 ),
948 )
949 .subcommand(
950 Command::new("delete")
951 .about("Remove the stored password for an agent from the OS keychain")
952 .arg(
953 Arg::new("agent-id")
954 .long("agent-id")
955 .help("Agent ID whose password to delete")
956 .value_name("AGENT_ID")
957 .required(true),
958 ),
959 )
960 .subcommand(
961 Command::new("status")
962 .about("Check if a password is stored for an agent in the OS keychain")
963 .arg(
964 Arg::new("agent-id")
965 .long("agent-id")
966 .help("Agent ID to check")
967 .value_name("AGENT_ID")
968 .required(true),
969 ),
970 )
971 .arg_required_else_help(true),
972 );
973
974 let cmd = cmd.subcommand(
975 Command::new("convert")
976 .about(
977 "Convert JACS documents between JSON, YAML, and HTML formats (no agent required)",
978 )
979 .arg(
980 Arg::new("to")
981 .long("to")
982 .required(true)
983 .value_parser(["json", "yaml", "html"])
984 .help("Target format: json, yaml, or html"),
985 )
986 .arg(
987 Arg::new("from")
988 .long("from")
989 .value_parser(["json", "yaml", "html"])
990 .help("Source format (auto-detected from extension if omitted)"),
991 )
992 .arg(
993 Arg::new("file")
994 .short('f')
995 .long("file")
996 .required(true)
997 .value_parser(value_parser!(String))
998 .help("Input file path (use '-' for stdin)"),
999 )
1000 .arg(
1001 Arg::new("output")
1002 .short('o')
1003 .long("output")
1004 .value_parser(value_parser!(String))
1005 .help("Output file path (defaults to stdout)"),
1006 ),
1007 );
1008
1009 let cmd = cmd
1011 .subcommand(
1012 Command::new("sign-text")
1013 .about("Sign a text/markdown file in place with an inline JACS signature")
1014 .arg(
1015 Arg::new("file")
1016 .help("Path to the text file to sign in place")
1017 .required(true)
1018 .value_parser(value_parser!(String)),
1019 )
1020 .arg(
1021 Arg::new("no-backup")
1022 .long("no-backup")
1023 .action(ArgAction::SetTrue)
1024 .help("Skip the automatic <path>.bak backup"),
1025 )
1026 .arg(
1027 Arg::new("json")
1028 .long("json")
1029 .action(ArgAction::SetTrue)
1030 .help("Output result as JSON"),
1031 ),
1032 )
1033 .subcommand(
1034 Command::new("verify-text")
1035 .about("Verify inline JACS signatures in a text/markdown file")
1036 .arg(
1037 Arg::new("file")
1038 .help("Path to the signed text file")
1039 .required(true)
1040 .value_parser(value_parser!(String)),
1041 )
1042 .arg(
1043 Arg::new("key-dir")
1044 .long("key-dir")
1045 .value_parser(value_parser!(String))
1046 .help("Directory containing signer public keys (.public.pem)"),
1047 )
1048 .arg(
1049 Arg::new("json")
1050 .long("json")
1051 .action(ArgAction::SetTrue)
1052 .help("Output result as JSON"),
1053 )
1054 .arg(
1055 Arg::new("strict")
1056 .long("strict")
1057 .action(ArgAction::SetTrue)
1058 .help(
1059 "Treat 'no JACS signature found' as a hard failure (exits 1 instead of 2)",
1060 ),
1061 ),
1062 )
1063 .subcommand(
1064 Command::new("sign-image")
1065 .about("Sign an image (PNG, JPEG, WebP) by embedding a JACS signature")
1066 .arg(
1067 Arg::new("input")
1068 .help("Path to the input image")
1069 .required(true)
1070 .value_parser(value_parser!(String)),
1071 )
1072 .arg(
1073 Arg::new("out")
1074 .long("out")
1075 .required(true)
1076 .value_parser(value_parser!(String))
1077 .help("Output image path"),
1078 )
1079 .arg(
1080 Arg::new("robust")
1081 .long("robust")
1082 .action(ArgAction::SetTrue)
1083 .help("Enable LSB fallback encoding (modifies pixel data; PNG/JPEG only)"),
1084 )
1085 .arg(
1086 Arg::new("format")
1087 .long("format")
1088 .value_parser(["png", "jpeg", "webp"])
1089 .help("Force a specific format (auto-detected by default)"),
1090 )
1091 .arg(
1092 Arg::new("refuse-overwrite")
1093 .long("refuse-overwrite")
1094 .action(ArgAction::SetTrue)
1095 .help("Refuse to overwrite an existing JACS signature on the input"),
1096 )
1097 .arg(
1098 Arg::new("json")
1099 .long("json")
1100 .action(ArgAction::SetTrue)
1101 .help("Output result as JSON"),
1102 ),
1103 )
1104 .subcommand(
1105 Command::new("verify-image")
1106 .about("Verify an embedded JACS signature in an image")
1107 .arg(
1108 Arg::new("file")
1109 .help("Path to the signed image")
1110 .required(true)
1111 .value_parser(value_parser!(String)),
1112 )
1113 .arg(
1114 Arg::new("key-dir")
1115 .long("key-dir")
1116 .value_parser(value_parser!(String))
1117 .help("Directory containing signer public keys (.public.pem)"),
1118 )
1119 .arg(
1120 Arg::new("json")
1121 .long("json")
1122 .action(ArgAction::SetTrue)
1123 .help("Output result as JSON"),
1124 )
1125 .arg(
1126 Arg::new("strict")
1127 .long("strict")
1128 .action(ArgAction::SetTrue)
1129 .help(
1130 "Treat 'no JACS signature found' as a hard failure (exits 1 instead of 2)",
1131 ),
1132 )
1133 .arg(
1134 Arg::new("robust")
1135 .long("robust")
1136 .action(ArgAction::SetTrue)
1137 .help("Scan LSB channel for the robust-mode payload (default off)"),
1138 ),
1139 )
1140 .subcommand(
1141 Command::new("extract-media-signature")
1142 .about("Extract the embedded JACS signature payload from an image")
1143 .arg(
1144 Arg::new("file")
1145 .help("Path to the image to extract from")
1146 .required(true)
1147 .value_parser(value_parser!(String)),
1148 )
1149 .arg(
1150 Arg::new("raw-payload")
1151 .long("raw-payload")
1152 .action(ArgAction::SetTrue)
1153 .help(
1154 "Print the raw base64url wire form instead of the decoded JSON",
1155 ),
1156 )
1157 .arg(
1158 Arg::new("robust")
1159 .long("robust")
1160 .action(ArgAction::SetTrue)
1161 .help(
1162 "Scan the LSB channel as a fallback if the metadata channel has \
1163 no payload (R-011; mirrors verify-image --robust)",
1164 ),
1165 ),
1166 );
1167
1168 cmd
1169}