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("List the agents that should sign a document's agreement (legacy v1; prefer `jacs agreement-v2`)")
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("Create a jacsAgreement sidecar on a document (legacy v1; prefer `jacs agreement-v2 create`)")
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("Sign a document's jacsAgreement sidecar (legacy v1; prefer `jacs agreement-v2 sign`)")
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("agreement-v2")
525                .about("Work with standalone JACS agreement v2 documents")
526                .subcommand_required(true)
527                .arg_required_else_help(true)
528                .long_about(
529                    "Work with standalone JACS agreement v2 documents.\n\n\
530                     An agreement v2 is a self-contained, cryptographically signed `jacsType: \"agreement\"` \
531                     artifact: terms, parties, a signature policy (quorum, required roles), an optional \
532                     transcript, links to related documents, controllers, and owners. Unlike the legacy \
533                     `jacs document *-agreement` sidecar commands, an agreement v2 is its own verifiable \
534                     document with a content hash and a version chain.\n\n\
535                     Typical workflow:\n  \
536                     1. create  -> build the agreement from a CreateAgreementV2 JSON input\n  \
537                     2. sign    -> each party adds a signer/witness/notary signature\n  \
538                     3. verify  -> recompute hashes, quorum, roles, and status before acting\n\n\
539                     Use `apply` to emit a successor version from a typed mutation (append transcript, \
540                     update terms, set status, etc.). Use `detect-conflict`, `merge-transcript`, and \
541                     `resolve-conflict` when two agents branch from the same prior version.",
542                )
543                .after_help(
544                    "Workflow: create -> sign -> verify. Use `apply` for mutations; \
545                     `detect-conflict`/`merge-transcript`/`resolve-conflict` for divergent branches.\n\
546                     Run `jacs agreement-v2 <SUBCOMMAND> --help` for per-command flags.",
547                )
548                .subcommand(
549                    Command::new("create")
550                        .about("Create a standalone agreement v2 document")
551                        .arg(
552                            Arg::new("input")
553                                .short('i')
554                                .long("input")
555                                .required(true)
556                                .value_parser(value_parser!(String))
557                                .help("CreateAgreementV2 JSON, path to JSON, or '-' for stdin"),
558                        ),
559                )
560                .subcommand(
561                    Command::new("apply")
562                        .about("Apply an agreement v2 mutation and emit a successor version")
563                        .arg(
564                            Arg::new("agreement")
565                                .short('a')
566                                .long("agreement")
567                                .required(true)
568                                .value_parser(value_parser!(String))
569                                .help("Agreement JSON, path to JSON, or '-' for stdin"),
570                        )
571                        .arg(
572                            Arg::new("mutation")
573                                .short('m')
574                                .long("mutation")
575                                .required(true)
576                                .value_parser(value_parser!(String))
577                                .help("Mutation JSON or path to mutation JSON"),
578                        ),
579                )
580                .subcommand(
581                    Command::new("sign")
582                        .about("Sign an agreement v2 document as signer, witness, or notary")
583                        .arg(
584                            Arg::new("agreement")
585                                .short('a')
586                                .long("agreement")
587                                .required(true)
588                                .value_parser(value_parser!(String))
589                                .help("Agreement JSON, path to JSON, or '-' for stdin"),
590                        )
591                        .arg(
592                            Arg::new("role")
593                                .long("role")
594                                .value_parser(["signer", "witness", "notary"])
595                                .default_value("signer")
596                                .help("Agreement signature role"),
597                        ),
598                )
599                .subcommand(
600                    Command::new("verify")
601                        .about("Verify an agreement v2 document")
602                        .arg(
603                            Arg::new("agreement")
604                                .short('a')
605                                .long("agreement")
606                                .required(true)
607                                .value_parser(value_parser!(String))
608                                .help("Agreement JSON, path to JSON, or '-' for stdin"),
609                        ),
610                )
611                .subcommand(
612                    Command::new("detect-conflict")
613                        .about("Detect whether two agreement v2 branches can auto-merge")
614                        .arg(
615                            Arg::new("base")
616                                .long("base")
617                                .required(true)
618                                .value_parser(value_parser!(String))
619                                .help("Common ancestor agreement (JSON, path, or '-' for stdin)"),
620                        )
621                        .arg(
622                            Arg::new("left")
623                                .long("left")
624                                .required(true)
625                                .value_parser(value_parser!(String))
626                                .help("One branch successor of base (JSON, path, or '-' for stdin)"),
627                        )
628                        .arg(
629                            Arg::new("right")
630                                .long("right")
631                                .required(true)
632                                .value_parser(value_parser!(String))
633                                .help("Other branch successor of base (JSON, path, or '-' for stdin)"),
634                        ),
635                )
636                .subcommand(
637                    Command::new("merge-transcript")
638                        .about("Auto-merge two transcript-only agreement v2 branches")
639                        .arg(
640                            Arg::new("base")
641                                .long("base")
642                                .required(true)
643                                .value_parser(value_parser!(String))
644                                .help("Common ancestor agreement (JSON, path, or '-' for stdin)"),
645                        )
646                        .arg(
647                            Arg::new("left")
648                                .long("left")
649                                .required(true)
650                                .value_parser(value_parser!(String))
651                                .help("One transcript-only branch successor (JSON, path, or '-' for stdin)"),
652                        )
653                        .arg(
654                            Arg::new("right")
655                                .long("right")
656                                .required(true)
657                                .value_parser(value_parser!(String))
658                                .help("Other transcript-only branch successor (JSON, path, or '-' for stdin)"),
659                        ),
660                )
661                .subcommand(
662                    Command::new("resolve-conflict")
663                        .about("Resolve an agreement v2 branch conflict with an explicit mutation")
664                        .arg(
665                            Arg::new("base")
666                                .long("base")
667                                .required(true)
668                                .value_parser(value_parser!(String))
669                                .help("Common ancestor agreement (JSON, path, or '-' for stdin)"),
670                        )
671                        .arg(
672                            Arg::new("previous")
673                                .long("previous")
674                                .required(true)
675                                .value_parser(value_parser!(String))
676                                .help("Branch you are keeping/rebasing onto (JSON, path, or '-' for stdin)"),
677                        )
678                        .arg(
679                            Arg::new("side")
680                                .long("side-branch")
681                                .alias("side")
682                                .required(true)
683                                .value_parser(value_parser!(String))
684                                .help("Divergent branch whose changes are being reconciled; \
685                                       recorded as a link on the resolution (JSON, path, or '-' for stdin). \
686                                       Alias: --side"),
687                        )
688                        .arg(
689                            Arg::new("mutation")
690                                .short('m')
691                                .long("mutation")
692                                .required(true)
693                                .value_parser(value_parser!(String))
694                                .help("Explicit resolving mutation (JSON or path to JSON)"),
695                        ),
696                )
697        )
698        .subcommand(
699            Command::new("key")
700                .about("Work with JACS cryptographic keys")
701                .subcommand(
702                    Command::new("reencrypt")
703                        .about("Re-encrypt the private key with a new password")
704                )
705        )
706        .subcommand(
707            Command::new("mcp")
708                .about("Start the built-in JACS MCP server (stdio transport)")
709                .arg(
710                    Arg::new("profile")
711                        .long("profile")
712                        .default_value("core")
713                        .help(
714                            "Tool profile: 'core' (default; document, trust, search, key, w3c tools) or \
715                             'full' (adds agreement, a2a, and attestation tools). Agreement v2 tools are \
716                             only registered under 'full' (or JACS_MCP_PROFILE=full).",
717                        ),
718                )
719                .subcommand(
720                    Command::new("install")
721                        .about("Deprecated: MCP is now built into the jacs binary")
722                        .hide(true)
723                )
724                .subcommand(
725                    Command::new("run")
726                        .about("Deprecated: use `jacs mcp` directly")
727                        .hide(true)
728                ),
729        )
730        .subcommand(
731            Command::new("a2a")
732                .about("A2A (Agent-to-Agent) trust and discovery commands")
733                .subcommand(
734                    Command::new("assess")
735                        .about("Assess trust level of a remote A2A Agent Card")
736                        .arg(
737                            Arg::new("source")
738                                .required(true)
739                                .help("Path to Agent Card JSON file or URL"),
740                        )
741                        .arg(
742                            Arg::new("policy")
743                                .long("policy")
744                                .short('p')
745                                .value_parser(["open", "verified", "strict"])
746                                .default_value("verified")
747                                .help("Trust policy to apply (default: verified)"),
748                        )
749                        .arg(
750                            Arg::new("json")
751                                .long("json")
752                                .action(ArgAction::SetTrue)
753                                .help("Output result as JSON"),
754                        ),
755                )
756                .subcommand(
757                    Command::new("trust")
758                        .about("Add a remote A2A agent to the local trust store")
759                        .arg(
760                            Arg::new("source")
761                                .required(true)
762                                .help("Path to Agent Card JSON file or URL"),
763                        ),
764                )
765                .subcommand(
766                    Command::new("discover")
767                        .about("Discover a remote A2A agent via its well-known Agent Card")
768                        .arg(
769                            Arg::new("url")
770                                .required(true)
771                                .help("Base URL of the agent (e.g. https://agent.example.com)"),
772                        )
773                        .arg(
774                            Arg::new("json")
775                                .long("json")
776                                .action(ArgAction::SetTrue)
777                                .help("Output the full Agent Card as JSON"),
778                        )
779                        .arg(
780                            Arg::new("policy")
781                                .long("policy")
782                                .short('p')
783                                .value_parser(["open", "verified", "strict"])
784                                .default_value("verified")
785                                .help("Trust policy to apply against the discovered card"),
786                        ),
787                )
788                .subcommand(
789                    Command::new("serve")
790                        .about("Serve this agent's .well-known endpoints for A2A discovery")
791                        .arg(
792                            Arg::new("port")
793                                .long("port")
794                                .value_parser(value_parser!(u16))
795                                .default_value("8080")
796                                .help("Port to listen on (default: 8080)"),
797                        )
798                        .arg(
799                            Arg::new("host")
800                                .long("host")
801                                .default_value("127.0.0.1")
802                                .help("Host to bind to (default: 127.0.0.1)"),
803                        ),
804                )
805                .subcommand(
806                    Command::new("quickstart")
807                        .about("Create/load an agent and start serving A2A endpoints (password required)")
808                        .after_help(quickstart_password_bootstrap_help())
809                        .arg(
810                            Arg::new("name")
811                                .long("name")
812                                .value_parser(value_parser!(String))
813                                .required(true)
814                                .help("Agent name used for first-time quickstart creation"),
815                        )
816                        .arg(
817                            Arg::new("domain")
818                                .long("domain")
819                                .value_parser(value_parser!(String))
820                                .required(true)
821                                .help("Agent domain used for DNS/public-key verification workflows"),
822                        )
823                        .arg(
824                            Arg::new("description")
825                                .long("description")
826                                .value_parser(value_parser!(String))
827                                .help("Optional human-readable agent description"),
828                        )
829                        .arg(
830                            Arg::new("port")
831                                .long("port")
832                                .value_parser(value_parser!(u16))
833                                .default_value("8080")
834                                .help("Port to listen on (default: 8080)"),
835                        )
836                        .arg(
837                            Arg::new("host")
838                                .long("host")
839                                .default_value("127.0.0.1")
840                                .help("Host to bind to (default: 127.0.0.1)"),
841                        )
842                        .arg(
843                            Arg::new("algorithm")
844                                .long("algorithm")
845                                .short('a')
846                                .value_parser(["pq2025", "ring-Ed25519"])
847                                .help("Signing algorithm (default: pq2025)"),
848                        ),
849                ),
850        )
851        .subcommand(
852            Command::new("w3c")
853                .about("W3C AI Agent Protocol interop helpers")
854                .subcommand(
855                    Command::new("did")
856                        .about("Export this agent's did:wba identifier")
857                        .arg(
858                            Arg::new("origin")
859                                .long("origin")
860                                .value_parser(value_parser!(String))
861                                .help("Controlling HTTPS origin for did:wba and discovery URLs"),
862                        ),
863                )
864                .subcommand(
865                    Command::new("did-document")
866                        .about("Export this agent's did:wba DID document")
867                        .arg(
868                            Arg::new("origin")
869                                .long("origin")
870                                .value_parser(value_parser!(String))
871                                .help("Controlling HTTPS origin for did:wba and discovery URLs"),
872                        ),
873                )
874                .subcommand(
875                    Command::new("agent-description")
876                        .about("Export this agent's W3C agent description document")
877                        .arg(
878                            Arg::new("origin")
879                                .long("origin")
880                                .value_parser(value_parser!(String))
881                                .help("Controlling HTTPS origin for did:wba and discovery URLs"),
882                        ),
883                )
884                .subcommand(
885                    Command::new("well-known")
886                        .about("Generate W3C well-known discovery documents")
887                        .arg(
888                            Arg::new("origin")
889                                .long("origin")
890                                .value_parser(value_parser!(String))
891                                .help("Controlling HTTPS origin for did:wba and discovery URLs"),
892                        )
893                        .arg(
894                            Arg::new("out")
895                                .long("out")
896                                .short('o')
897                                .value_parser(value_parser!(String))
898                                .help("Directory to write generated discovery files into"),
899                        ),
900                )
901                .subcommand(
902                    Command::new("serve")
903                        .about("Serve W3C discovery documents for local demo/testing")
904                        .arg(
905                            Arg::new("origin")
906                                .long("origin")
907                                .value_parser(value_parser!(String))
908                                .help("Controlling HTTPS origin for did:wba and discovery URLs"),
909                        )
910                        .arg(
911                            Arg::new("port")
912                                .long("port")
913                                .value_parser(value_parser!(u16))
914                                .default_value("8081")
915                                .help("Port to listen on (default: 8081)"),
916                        )
917                        .arg(
918                            Arg::new("host")
919                                .long("host")
920                                .default_value("127.0.0.1")
921                                .help("Host to bind to (default: 127.0.0.1)"),
922                        ),
923                )
924                .subcommand(
925                    Command::new("sign-request")
926                        .about("Create a request-bound DID authentication proof")
927                        .arg(
928                            Arg::new("method")
929                                .long("method")
930                                .value_parser(value_parser!(String))
931                                .required(true)
932                                .help("HTTP method to bind into the proof"),
933                        )
934                        .arg(
935                            Arg::new("url")
936                                .long("url")
937                                .value_parser(value_parser!(String))
938                                .required(true)
939                                .help("HTTP target URI to bind into the proof"),
940                        )
941                        .arg(
942                            Arg::new("body")
943                                .long("body")
944                                .value_parser(value_parser!(String))
945                                .conflicts_with("body-file")
946                                .help("Request body bytes to digest and bind"),
947                        )
948                        .arg(
949                            Arg::new("body-file")
950                                .long("body-file")
951                                .value_parser(value_parser!(String))
952                                .conflicts_with("body")
953                                .help("File containing request body bytes to digest and bind"),
954                        )
955                        .arg(
956                            Arg::new("origin")
957                                .long("origin")
958                                .value_parser(value_parser!(String))
959                                .help("Controlling HTTPS origin for the signing agent DID"),
960                        )
961                        .arg(
962                            Arg::new("nonce")
963                                .long("nonce")
964                                .value_parser(value_parser!(String))
965                                .help("Caller-provided nonce; generated when omitted"),
966                        )
967                        .arg(
968                            Arg::new("created")
969                                .long("created")
970                                .value_parser(value_parser!(String))
971                                .help("RFC3339 created time; current time when omitted"),
972                        ),
973                )
974                .subcommand(
975                    Command::new("verify-request")
976                        .about("Verify a request-bound DID authentication proof")
977                        .arg(
978                            Arg::new("method")
979                                .long("method")
980                                .value_parser(value_parser!(String))
981                                .help("Actual HTTP method to compare against the proof"),
982                        )
983                        .arg(
984                            Arg::new("url")
985                                .long("url")
986                                .value_parser(value_parser!(String))
987                                .help("Actual HTTP target URI to compare against the proof"),
988                        )
989                        .arg(
990                            Arg::new("proof")
991                                .long("proof")
992                                .value_parser(value_parser!(String))
993                                .required(true)
994                                .help("Path to request proof JSON"),
995                        )
996                        .arg(
997                            Arg::new("did-document")
998                                .long("did-document")
999                                .value_parser(value_parser!(String))
1000                                .required(true)
1001                                .help("Path to resolved DID document JSON"),
1002                        )
1003                        .arg(
1004                            Arg::new("body")
1005                                .long("body")
1006                                .value_parser(value_parser!(String))
1007                                .conflicts_with("body-file")
1008                                .help("Request body bytes to check against the proof digest"),
1009                        )
1010                        .arg(
1011                            Arg::new("body-file")
1012                                .long("body-file")
1013                                .value_parser(value_parser!(String))
1014                                .conflicts_with("body")
1015                                .help("File containing request body bytes to check"),
1016                        )
1017                        .arg(
1018                            Arg::new("max-age-seconds")
1019                                .long("max-age-seconds")
1020                                .value_parser(value_parser!(u64))
1021                                .default_value("300")
1022                                .help("Maximum accepted proof age in seconds"),
1023                        ),
1024                ),
1025        )
1026        .subcommand(
1027            Command::new("quickstart")
1028                .about("Create or load a persistent agent for instant sign/verify (password required)")
1029                .after_help(quickstart_password_bootstrap_help())
1030                .arg(
1031                    Arg::new("name")
1032                        .long("name")
1033                        .value_parser(value_parser!(String))
1034                        .required(true)
1035                        .help("Agent name used for first-time quickstart creation"),
1036                )
1037                .arg(
1038                    Arg::new("domain")
1039                        .long("domain")
1040                        .value_parser(value_parser!(String))
1041                        .required(true)
1042                        .help("Agent domain used for DNS/public-key verification workflows"),
1043                )
1044                .arg(
1045                    Arg::new("description")
1046                        .long("description")
1047                        .value_parser(value_parser!(String))
1048                        .help("Optional human-readable agent description"),
1049                )
1050                .arg(
1051                    Arg::new("algorithm")
1052                        .long("algorithm")
1053                        .short('a')
1054                        .value_parser(["ed25519", "pq2025"])
1055                        .default_value("pq2025")
1056                        .help("Signing algorithm (default: pq2025)"),
1057                )
1058                .arg(
1059                    Arg::new("sign")
1060                        .long("sign")
1061                        .help("Sign JSON from stdin and print signed document to stdout")
1062                        .action(ArgAction::SetTrue),
1063                )
1064                .arg(
1065                    Arg::new("file")
1066                        .short('f')
1067                        .long("file")
1068                        .value_parser(value_parser!(String))
1069                        .help("Sign a JSON file instead of reading from stdin (used with --sign)"),
1070                )
1071        )
1072        .subcommand(
1073            Command::new("init")
1074                .about("Initialize JACS by creating both config and agent (with keys)")
1075                .arg(
1076                    Arg::new("yes")
1077                        .long("yes")
1078                        .short('y')
1079                        .action(ArgAction::SetTrue)
1080                        .help("Automatically set the new agent ID in jacs.config.json without prompting"),
1081                )
1082        )
1083        .subcommand(
1084            Command::new("attest")
1085                .about("Create and verify attestation documents")
1086                .subcommand(
1087                    Command::new("create")
1088                        .about("Create a signed attestation")
1089                        .arg(
1090                            Arg::new("subject-type")
1091                                .long("subject-type")
1092                                .value_parser(["agent", "artifact", "workflow", "identity"])
1093                                .help("Type of subject being attested"),
1094                        )
1095                        .arg(
1096                            Arg::new("subject-id")
1097                                .long("subject-id")
1098                                .value_parser(value_parser!(String))
1099                                .help("Identifier of the subject"),
1100                        )
1101                        .arg(
1102                            Arg::new("subject-digest")
1103                                .long("subject-digest")
1104                                .value_parser(value_parser!(String))
1105                                .help("SHA-256 digest of the subject"),
1106                        )
1107                        .arg(
1108                            Arg::new("claims")
1109                                .long("claims")
1110                                .value_parser(value_parser!(String))
1111                                .required(true)
1112                                .help("JSON array of claims, e.g. '[{\"name\":\"reviewed\",\"value\":true}]'"),
1113                        )
1114                        .arg(
1115                            Arg::new("evidence")
1116                                .long("evidence")
1117                                .value_parser(value_parser!(String))
1118                                .help("JSON array of evidence references"),
1119                        )
1120                        .arg(
1121                            Arg::new("from-document")
1122                                .long("from-document")
1123                                .value_parser(value_parser!(String))
1124                                .help("Lift attestation from an existing signed document file"),
1125                        )
1126                        .arg(
1127                            Arg::new("output")
1128                                .short('o')
1129                                .long("output")
1130                                .value_parser(value_parser!(String))
1131                                .help("Write attestation to file instead of stdout"),
1132                        ),
1133                )
1134                .subcommand(
1135                    Command::new("verify")
1136                        .about("Verify an attestation document")
1137                        .arg(
1138                            Arg::new("file")
1139                                .help("Path to the attestation JSON file")
1140                                .required(true)
1141                                .value_parser(value_parser!(String)),
1142                        )
1143                        .arg(
1144                            Arg::new("full")
1145                                .long("full")
1146                                .action(ArgAction::SetTrue)
1147                                .help("Use full verification (evidence + derivation chain)"),
1148                        )
1149                        .arg(
1150                            Arg::new("json")
1151                                .long("json")
1152                                .action(ArgAction::SetTrue)
1153                                .help("Output result as JSON"),
1154                        )
1155                        .arg(
1156                            Arg::new("key-dir")
1157                                .long("key-dir")
1158                                .value_parser(value_parser!(String))
1159                                .help("Directory containing public keys for verification"),
1160                        )
1161                        .arg(
1162                            Arg::new("max-depth")
1163                                .long("max-depth")
1164                                .value_parser(value_parser!(u32))
1165                                .help("Maximum derivation chain depth"),
1166                        ),
1167                )
1168                .subcommand(
1169                    Command::new("export-dsse")
1170                        .about("Export an attestation as a DSSE envelope for in-toto/SLSA")
1171                        .arg(
1172                            Arg::new("file")
1173                                .help("Path to the signed attestation JSON file")
1174                                .required(true)
1175                                .value_parser(value_parser!(String)),
1176                        )
1177                        .arg(
1178                            Arg::new("output")
1179                                .short('o')
1180                                .long("output")
1181                                .value_parser(value_parser!(String))
1182                                .help("Write DSSE envelope to file instead of stdout"),
1183                        ),
1184                )
1185                .subcommand_required(true)
1186                .arg_required_else_help(true),
1187        )
1188        .subcommand(
1189            Command::new("verify")
1190                .about("Verify a signed JACS document (no agent required)")
1191                .arg(
1192                    Arg::new("file")
1193                        .help("Path to the signed JACS JSON file")
1194                        .required_unless_present("remote")
1195                        .value_parser(value_parser!(String)),
1196                )
1197                .arg(
1198                    Arg::new("remote")
1199                        .long("remote")
1200                        .value_parser(value_parser!(String))
1201                        .help("Fetch document from URL before verifying"),
1202                )
1203                .arg(
1204                    Arg::new("json")
1205                        .long("json")
1206                        .action(ArgAction::SetTrue)
1207                        .help("Output result as JSON"),
1208                )
1209                .arg(
1210                    Arg::new("key-dir")
1211                        .long("key-dir")
1212                        .value_parser(value_parser!(String))
1213                        .help("Directory containing public keys for verification"),
1214                )
1215        );
1216
1217    // OS keychain subcommand (only when keychain feature is enabled)
1218    #[cfg(feature = "keychain")]
1219    let cmd = cmd.subcommand(
1220        Command::new("keychain")
1221            .about("Manage private key passwords in the OS keychain (per-agent)")
1222            .subcommand(
1223                Command::new("set")
1224                    .about("Store a password in the OS keychain for an agent")
1225                    .arg(
1226                        Arg::new("agent-id")
1227                            .long("agent-id")
1228                            .help("Agent ID to associate the password with")
1229                            .value_name("AGENT_ID")
1230                            .required(true),
1231                    )
1232                    .arg(
1233                        Arg::new("password")
1234                            .long("password")
1235                            .help("Password to store (if omitted, prompts interactively)")
1236                            .value_name("PASSWORD"),
1237                    ),
1238            )
1239            .subcommand(
1240                Command::new("get")
1241                    .about("Retrieve the stored password for an agent (prints to stdout)")
1242                    .arg(
1243                        Arg::new("agent-id")
1244                            .long("agent-id")
1245                            .help("Agent ID to look up")
1246                            .value_name("AGENT_ID")
1247                            .required(true),
1248                    ),
1249            )
1250            .subcommand(
1251                Command::new("delete")
1252                    .about("Remove the stored password for an agent from the OS keychain")
1253                    .arg(
1254                        Arg::new("agent-id")
1255                            .long("agent-id")
1256                            .help("Agent ID whose password to delete")
1257                            .value_name("AGENT_ID")
1258                            .required(true),
1259                    ),
1260            )
1261            .subcommand(
1262                Command::new("status")
1263                    .about("Check if a password is stored for an agent in the OS keychain")
1264                    .arg(
1265                        Arg::new("agent-id")
1266                            .long("agent-id")
1267                            .help("Agent ID to check")
1268                            .value_name("AGENT_ID")
1269                            .required(true),
1270                    ),
1271            )
1272            .arg_required_else_help(true),
1273    );
1274
1275    let cmd = cmd.subcommand(
1276        Command::new("convert")
1277            .about(
1278                "Convert JACS documents between JSON, YAML, and HTML formats (no agent required)",
1279            )
1280            .arg(
1281                Arg::new("to")
1282                    .long("to")
1283                    .required(true)
1284                    .value_parser(["json", "yaml", "html"])
1285                    .help("Target format: json, yaml, or html"),
1286            )
1287            .arg(
1288                Arg::new("from")
1289                    .long("from")
1290                    .value_parser(["json", "yaml", "html"])
1291                    .help("Source format (auto-detected from extension if omitted)"),
1292            )
1293            .arg(
1294                Arg::new("file")
1295                    .short('f')
1296                    .long("file")
1297                    .required(true)
1298                    .value_parser(value_parser!(String))
1299                    .help("Input file path (use '-' for stdin)"),
1300            )
1301            .arg(
1302                Arg::new("output")
1303                    .short('o')
1304                    .long("output")
1305                    .value_parser(value_parser!(String))
1306                    .help("Output file path (defaults to stdout)"),
1307            ),
1308    );
1309
1310    // Inline text + media verbs (Task 08, PRD §3.1 / §3.2 / §4.1 / §4.2).
1311
1312    cmd.subcommand(
1313        Command::new("sign-text")
1314            .about("Sign a text/markdown file in place with an inline JACS signature")
1315            .arg(
1316                Arg::new("file")
1317                    .help("Path to the text file to sign in place")
1318                    .required(true)
1319                    .value_parser(value_parser!(String)),
1320            )
1321            .arg(
1322                Arg::new("no-backup")
1323                    .long("no-backup")
1324                    .action(ArgAction::SetTrue)
1325                    .help("Skip the automatic <path>.bak backup"),
1326            )
1327            .arg(
1328                Arg::new("json")
1329                    .long("json")
1330                    .action(ArgAction::SetTrue)
1331                    .help("Output result as JSON"),
1332            ),
1333    )
1334    .subcommand(
1335        Command::new("verify-text")
1336            .about("Verify inline JACS signatures in a text/markdown file")
1337            .arg(
1338                Arg::new("file")
1339                    .help("Path to the signed text file")
1340                    .required(true)
1341                    .value_parser(value_parser!(String)),
1342            )
1343            .arg(
1344                Arg::new("key-dir")
1345                    .long("key-dir")
1346                    .value_parser(value_parser!(String))
1347                    .help("Directory containing signer public keys (.public.pem)"),
1348            )
1349            .arg(
1350                Arg::new("json")
1351                    .long("json")
1352                    .action(ArgAction::SetTrue)
1353                    .help("Output result as JSON"),
1354            )
1355            .arg(
1356                Arg::new("strict")
1357                    .long("strict")
1358                    .action(ArgAction::SetTrue)
1359                    .help(
1360                        "Treat 'no JACS signature found' as a hard failure (exits 1 instead of 2)",
1361                    ),
1362            ),
1363    )
1364    .subcommand(
1365        Command::new("sign-image")
1366            .about("Sign an image (PNG, JPEG, WebP) by embedding a JACS signature")
1367            .arg(
1368                Arg::new("input")
1369                    .help("Path to the input image")
1370                    .required(true)
1371                    .value_parser(value_parser!(String)),
1372            )
1373            .arg(
1374                Arg::new("out")
1375                    .long("out")
1376                    .required(true)
1377                    .value_parser(value_parser!(String))
1378                    .help("Output image path"),
1379            )
1380            .arg(
1381                Arg::new("robust")
1382                    .long("robust")
1383                    .action(ArgAction::SetTrue)
1384                    .help("Enable LSB fallback encoding (modifies pixel data; PNG/JPEG only)"),
1385            )
1386            .arg(
1387                Arg::new("format")
1388                    .long("format")
1389                    .value_parser(["png", "jpeg", "webp"])
1390                    .help("Force a specific format (auto-detected by default)"),
1391            )
1392            .arg(
1393                Arg::new("refuse-overwrite")
1394                    .long("refuse-overwrite")
1395                    .action(ArgAction::SetTrue)
1396                    .help("Refuse to overwrite an existing JACS signature on the input"),
1397            )
1398            .arg(
1399                Arg::new("json")
1400                    .long("json")
1401                    .action(ArgAction::SetTrue)
1402                    .help("Output result as JSON"),
1403            ),
1404    )
1405    .subcommand(
1406        Command::new("verify-image")
1407            .about("Verify an embedded JACS signature in an image")
1408            .arg(
1409                Arg::new("file")
1410                    .help("Path to the signed image")
1411                    .required(true)
1412                    .value_parser(value_parser!(String)),
1413            )
1414            .arg(
1415                Arg::new("key-dir")
1416                    .long("key-dir")
1417                    .value_parser(value_parser!(String))
1418                    .help("Directory containing signer public keys (.public.pem)"),
1419            )
1420            .arg(
1421                Arg::new("json")
1422                    .long("json")
1423                    .action(ArgAction::SetTrue)
1424                    .help("Output result as JSON"),
1425            )
1426            .arg(
1427                Arg::new("strict")
1428                    .long("strict")
1429                    .action(ArgAction::SetTrue)
1430                    .help(
1431                        "Treat 'no JACS signature found' as a hard failure (exits 1 instead of 2)",
1432                    ),
1433            )
1434            .arg(
1435                Arg::new("robust")
1436                    .long("robust")
1437                    .action(ArgAction::SetTrue)
1438                    .help("Scan LSB channel for the robust-mode payload (default off)"),
1439            ),
1440    )
1441    .subcommand(
1442        Command::new("extract-media-signature")
1443            .about("Extract the embedded JACS signature payload from an image")
1444            .arg(
1445                Arg::new("file")
1446                    .help("Path to the image to extract from")
1447                    .required(true)
1448                    .value_parser(value_parser!(String)),
1449            )
1450            .arg(
1451                Arg::new("raw-payload")
1452                    .long("raw-payload")
1453                    .action(ArgAction::SetTrue)
1454                    .help("Print the raw base64url wire form instead of the decoded JSON"),
1455            )
1456            .arg(
1457                Arg::new("robust")
1458                    .long("robust")
1459                    .action(ArgAction::SetTrue)
1460                    .help(
1461                        "Scan the LSB channel as a fallback if the metadata channel has \
1462                             no payload (R-011; mirrors verify-image --robust)",
1463                    ),
1464            ),
1465    )
1466}