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("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    // OS keychain subcommand (only when keychain feature is enabled)
917    #[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    // Inline text + media verbs (Task 08, PRD §3.1 / §3.2 / §4.1 / §4.2).
1010    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}