Skip to main content

jacs_cli/
cli_builder.rs

1//! Clap `Command` tree for the `jacs` binary.
2//!
3//! Extracted from `main.rs` so the library target (used by the snapshot test
4//! in `tests/cli_command_snapshot.rs`) can pick it up without dragging in the
5//! full binary entry point. See `src/lib.rs` for the public re-export.
6
7use 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    // OS keychain subcommand (only when keychain feature is enabled)
864    #[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    // Inline text + media verbs (Task 08, PRD §3.1 / §3.2 / §4.1 / §4.2).
957
958    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}