1use clap::{Arg, ArgAction, Command, crate_name, value_parser};
2
3mod agent_loader;
4mod password_bootstrap;
5
6use agent_loader::{load_agent, load_agent_with_cli_dns_policy};
7use jacs::agent::Agent;
8use jacs::agent::boilerplate::BoilerPlate;
9use jacs::agent::document::DocumentTraits;
10use jacs::cli_utils::create::{
11 handle_agent_create, handle_agent_create_auto, handle_config_create,
12};
13use jacs::cli_utils::default_set_file_list;
14use jacs::cli_utils::document::{
15 check_agreement, create_agreement, create_documents, extract_documents, sign_documents,
16 update_documents, verify_documents,
17};
18use jacs::create_task; use jacs::dns::bootstrap as dns_bootstrap;
20use jacs::shutdown::{ShutdownGuard, install_signal_handler};
21use password_bootstrap::{
22 ensure_cli_private_key_password, quickstart_password_bootstrap_help,
23 wrap_quickstart_error_with_password_help,
24};
25
26use rpassword::read_password;
27use std::env;
28use std::error::Error;
29use std::process;
30
31pub fn build_cli() -> Command {
38 let cmd = Command::new(crate_name!())
39 .version(env!("CARGO_PKG_VERSION"))
40 .about(env!("CARGO_PKG_DESCRIPTION"))
41 .subcommand(
42 Command::new("version")
43 .about("Prints version and build information")
44 )
45 .subcommand(
46 Command::new("config")
47 .about(" work with JACS configuration")
48 .subcommand(
49 Command::new("create")
50 .about(" create a config file")
51 )
52 .subcommand(
53 Command::new("read")
54 .about("read configuration and display to screen. This includes both the config file and the env variables.")
55 ),
56 )
57 .subcommand(
58 Command::new("agent")
59 .about(" work with a JACS agent")
60 .subcommand(
61 Command::new("dns")
62 .about("emit DNS TXT commands for publishing agent fingerprint")
63 .arg(
64 Arg::new("agent-file")
65 .short('a')
66 .long("agent-file")
67 .value_parser(value_parser!(String))
68 .help("Path to agent JSON (optional; defaults via config)"),
69 )
70 .arg(
71 Arg::new("no-dns")
72 .long("no-dns")
73 .help("Disable DNS validation; rely on embedded fingerprint")
74 .action(ArgAction::SetTrue),
75 )
76 .arg(
77 Arg::new("require-dns")
78 .long("require-dns")
79 .help("Require DNS validation; if domain missing, fail. Not strict (no DNSSEC required).")
80 .action(ArgAction::SetTrue),
81 )
82 .arg(
83 Arg::new("require-strict-dns")
84 .long("require-strict-dns")
85 .help("Require strict DNSSEC validation; if domain missing, fail.")
86 .action(ArgAction::SetTrue),
87 )
88 .arg(
89 Arg::new("ignore-dns")
90 .long("ignore-dns")
91 .help("Ignore DNS validation entirely.")
92 .action(ArgAction::SetTrue),
93 )
94 .arg(Arg::new("domain").long("domain").value_parser(value_parser!(String)))
95 .arg(Arg::new("agent-id").long("agent-id").value_parser(value_parser!(String)))
96 .arg(Arg::new("ttl").long("ttl").value_parser(value_parser!(u32)).default_value("3600"))
97 .arg(Arg::new("encoding").long("encoding").value_parser(["base64","hex"]).default_value("base64"))
98 .arg(Arg::new("provider").long("provider").value_parser(["plain","aws","azure","cloudflare"]).default_value("plain"))
99 )
100 .subcommand(
101 Command::new("create")
102 .about(" create an agent")
103 .arg(
104 Arg::new("filename")
105 .short('f')
106 .help("Name of the json file with agent schema and jacsAgentType")
107 .value_parser(value_parser!(String)),
108 )
109 .arg(
110 Arg::new("create-keys")
111 .long("create-keys")
112 .required(true)
113 .help("Create keys or not if they already exist. Configure key type in jacs.config.json")
114 .value_parser(value_parser!(bool)),
115 ),
116 )
117 .subcommand(
118 Command::new("verify")
119 .about(" verify an agent")
120 .arg(
121 Arg::new("agent-file")
122 .short('a')
123 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
124 .value_parser(value_parser!(String)),
125 )
126 .arg(
127 Arg::new("no-dns")
128 .long("no-dns")
129 .help("Disable DNS validation; rely on embedded fingerprint")
130 .action(ArgAction::SetTrue),
131 )
132 .arg(
133 Arg::new("require-dns")
134 .long("require-dns")
135 .help("Require DNS validation; if domain missing, fail. Not strict (no DNSSEC required).")
136 .action(ArgAction::SetTrue),
137 )
138 .arg(
139 Arg::new("require-strict-dns")
140 .long("require-strict-dns")
141 .help("Require strict DNSSEC validation; if domain missing, fail.")
142 .action(ArgAction::SetTrue),
143 )
144 .arg(
145 Arg::new("ignore-dns")
146 .long("ignore-dns")
147 .help("Ignore DNS validation entirely.")
148 .action(ArgAction::SetTrue),
149 ),
150 )
151 .subcommand(
152 Command::new("lookup")
153 .about("Look up another agent's public key and DNS info from their domain")
154 .arg(
155 Arg::new("domain")
156 .required(true)
157 .help("Domain to look up (e.g., agent.example.com)"),
158 )
159 .arg(
160 Arg::new("no-dns")
161 .long("no-dns")
162 .help("Skip DNS TXT record lookup")
163 .action(ArgAction::SetTrue),
164 )
165 .arg(
166 Arg::new("strict")
167 .long("strict")
168 .help("Require DNSSEC validation for DNS lookup")
169 .action(ArgAction::SetTrue),
170 ),
171 )
172 .subcommand(
173 Command::new("rotate-keys")
174 .about("Rotate the agent's cryptographic keys")
175 .arg(
176 Arg::new("algorithm")
177 .long("algorithm")
178 .value_parser(["ring-Ed25519", "pq2025"])
179 .help("Signing algorithm for the new keys (defaults to current)"),
180 )
181 .arg(
182 Arg::new("config")
183 .long("config")
184 .value_parser(value_parser!(String))
185 .help("Path to jacs.config.json (default: ./jacs.config.json)"),
186 ),
187 )
188 .subcommand(
189 Command::new("keys-list")
190 .about("List active and archived key files")
191 .arg(
192 Arg::new("config")
193 .long("config")
194 .value_parser(value_parser!(String))
195 .help("Path to jacs.config.json (default: ./jacs.config.json)"),
196 ),
197 )
198 .subcommand(
199 Command::new("repair")
200 .about("Repair config after an interrupted key rotation")
201 .arg(
202 Arg::new("config")
203 .long("config")
204 .value_parser(value_parser!(String))
205 .help("Path to jacs.config.json (default: ./jacs.config.json)"),
206 ),
207 ),
208 )
209
210 .subcommand(
211 Command::new("task")
212 .about(" work with a JACS Agent task")
213 .subcommand(
214 Command::new("create")
215 .about(" create a new JACS Task file, either by embedding or parsing a document")
216 .arg(
217 Arg::new("agent-file")
218 .short('a')
219 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
220 .value_parser(value_parser!(String)),
221 )
222 .arg(
223 Arg::new("filename")
224 .short('f')
225 .help("Path to input file. Must be JSON")
226 .value_parser(value_parser!(String)),
227 )
228 .arg(
229 Arg::new("name")
230 .short('n')
231 .required(true)
232 .help("name of task")
233 .value_parser(value_parser!(String)),
234 )
235 .arg(
236 Arg::new("description")
237 .short('d')
238 .required(true)
239 .help("description of task")
240 .value_parser(value_parser!(String)),
241 )
242 )
243 .subcommand(
244 Command::new("update")
245 .about("update an existing task document")
246 .arg(
247 Arg::new("filename")
248 .short('f')
249 .required(true)
250 .help("Path to the updated task JSON file")
251 .value_parser(value_parser!(String)),
252 )
253 .arg(
254 Arg::new("task-key")
255 .short('k')
256 .required(true)
257 .help("Task document key (id:version)")
258 .value_parser(value_parser!(String)),
259 )
260 )
261 )
262
263 .subcommand(
264 Command::new("document")
265 .about(" work with a general JACS document")
266 .subcommand(
267 Command::new("create")
268 .about(" create a new JACS file, either by embedding or parsing a document")
269 .arg(
270 Arg::new("agent-file")
271 .short('a')
272 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
273 .value_parser(value_parser!(String)),
274 )
275 .arg(
276 Arg::new("filename")
277 .short('f')
278 .help("Path to input file. Must be JSON")
279 .value_parser(value_parser!(String)),
280 )
281 .arg(
282 Arg::new("output")
283 .short('o')
284 .help("Output filename. ")
285 .value_parser(value_parser!(String)),
286 )
287 .arg(
288 Arg::new("directory")
289 .short('d')
290 .help("Path to directory of files. Files should end with .json")
291 .value_parser(value_parser!(String)),
292 )
293 .arg(
294 Arg::new("verbose")
295 .short('v')
296 .long("verbose")
297 .action(ArgAction::SetTrue),
298 )
299 .arg(
300 Arg::new("no-save")
301 .long("no-save")
302 .short('n')
303 .help("Instead of saving files, print to stdout")
304 .action(ArgAction::SetTrue),
305 )
306 .arg(
307 Arg::new("schema")
308 .short('s')
309 .help("Path to JSON schema file to use to create")
310 .long("schema")
311 .value_parser(value_parser!(String)),
312 )
313 .arg(
314 Arg::new("attach")
315 .help("Path to file or directory for file attachments")
316 .long("attach")
317 .value_parser(value_parser!(String)),
318 )
319 .arg(
320 Arg::new("embed")
321 .short('e')
322 .help("Embed documents or keep the documents external")
323 .long("embed")
324 .value_parser(value_parser!(bool)),
325 ),
326 )
327 .subcommand(
328 Command::new("update")
329 .about("create a new version of document. requires both the original JACS file and the modified jacs metadata")
330 .arg(
331 Arg::new("agent-file")
332 .short('a')
333 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
334 .value_parser(value_parser!(String)),
335 )
336 .arg(
337 Arg::new("new")
338 .short('n')
339 .required(true)
340 .help("Path to new version of document.")
341 .value_parser(value_parser!(String)),
342 )
343 .arg(
344 Arg::new("filename")
345 .short('f')
346 .required(true)
347 .help("Path to original document.")
348 .value_parser(value_parser!(String)),
349 )
350 .arg(
351 Arg::new("output")
352 .short('o')
353 .help("Output filename. Filenames will always end with \"json\"")
354 .value_parser(value_parser!(String)),
355 )
356 .arg(
357 Arg::new("verbose")
358 .short('v')
359 .long("verbose")
360 .action(ArgAction::SetTrue),
361 )
362 .arg(
363 Arg::new("no-save")
364 .long("no-save")
365 .short('n')
366 .help("Instead of saving files, print to stdout")
367 .action(ArgAction::SetTrue),
368 )
369 .arg(
370 Arg::new("schema")
371 .short('s')
372 .help("Path to JSON schema file to use to create")
373 .long("schema")
374 .value_parser(value_parser!(String)),
375 )
376 .arg(
377 Arg::new("attach")
378 .help("Path to file or directory for file attachments")
379 .long("attach")
380 .value_parser(value_parser!(String)),
381 )
382 .arg(
383 Arg::new("embed")
384 .short('e')
385 .help("Embed documents or keep the documents external")
386 .long("embed")
387 .value_parser(value_parser!(bool)),
388 )
389 ,
390 )
391 .subcommand(
392 Command::new("check-agreement")
393 .about("given a document, provide alist of agents that should sign document")
394 .arg(
395 Arg::new("agent-file")
396 .short('a')
397 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
398 .value_parser(value_parser!(String)),
399 )
400 .arg(
401 Arg::new("filename")
402 .short('f')
403 .required(true)
404 .help("Path to original document.")
405 .value_parser(value_parser!(String)),
406 )
407 .arg(
408 Arg::new("directory")
409 .short('d')
410 .help("Path to directory of files. Files should end with .json")
411 .value_parser(value_parser!(String)),
412 )
413 .arg(
414 Arg::new("schema")
415 .short('s')
416 .help("Path to JSON schema file to use to create")
417 .long("schema")
418 .value_parser(value_parser!(String)),
419 )
420
421 )
422 .subcommand(
423 Command::new("create-agreement")
424 .about("given a document, provide alist of agents that should sign document")
425 .arg(
426 Arg::new("agent-file")
427 .short('a')
428 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
429 .value_parser(value_parser!(String)),
430 )
431 .arg(
432 Arg::new("filename")
433 .short('f')
434 .required(true)
435 .help("Path to original document.")
436 .value_parser(value_parser!(String)),
437 )
438 .arg(
439 Arg::new("directory")
440 .short('d')
441 .help("Path to directory of files. Files should end with .json")
442 .value_parser(value_parser!(String)),
443 )
444 .arg(
445 Arg::new("agentids")
446 .short('i')
447 .long("agentids")
448 .value_name("VALUES")
449 .help("Comma-separated list of agent ids")
450 .value_delimiter(',')
451 .required(true)
452 .action(clap::ArgAction::Set),
453 )
454 .arg(
455 Arg::new("output")
456 .short('o')
457 .help("Output filename. Filenames will always end with \"json\"")
458 .value_parser(value_parser!(String)),
459 )
460 .arg(
461 Arg::new("verbose")
462 .short('v')
463 .long("verbose")
464 .action(ArgAction::SetTrue),
465 )
466 .arg(
467 Arg::new("no-save")
468 .long("no-save")
469 .short('n')
470 .help("Instead of saving files, print to stdout")
471 .action(ArgAction::SetTrue),
472 )
473 .arg(
474 Arg::new("schema")
475 .short('s')
476 .help("Path to JSON schema file to use to create")
477 .long("schema")
478 .value_parser(value_parser!(String)),
479 )
480
481 ).subcommand(
482 Command::new("sign-agreement")
483 .about("given a document, sign the agreement section")
484 .arg(
485 Arg::new("agent-file")
486 .short('a')
487 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
488 .value_parser(value_parser!(String)),
489 )
490 .arg(
491 Arg::new("filename")
492 .short('f')
493 .required(true)
494 .help("Path to original document.")
495 .value_parser(value_parser!(String)),
496 )
497 .arg(
498 Arg::new("directory")
499 .short('d')
500 .help("Path to directory of files. Files should end with .json")
501 .value_parser(value_parser!(String)),
502 )
503 .arg(
504 Arg::new("output")
505 .short('o')
506 .help("Output filename. Filenames will always end with \"json\"")
507 .value_parser(value_parser!(String)),
508 )
509 .arg(
510 Arg::new("verbose")
511 .short('v')
512 .long("verbose")
513 .action(ArgAction::SetTrue),
514 )
515 .arg(
516 Arg::new("no-save")
517 .long("no-save")
518 .short('n')
519 .help("Instead of saving files, print to stdout")
520 .action(ArgAction::SetTrue),
521 )
522 .arg(
523 Arg::new("schema")
524 .short('s')
525 .help("Path to JSON schema file to use to create")
526 .long("schema")
527 .value_parser(value_parser!(String)),
528 )
529
530 )
531 .subcommand(
532 Command::new("verify")
533 .about(" verify a documents hash, siginatures, and schema")
534 .arg(
535 Arg::new("agent-file")
536 .short('a')
537 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
538 .value_parser(value_parser!(String)),
539 )
540 .arg(
541 Arg::new("filename")
542 .short('f')
543 .help("Path to input file. Must be JSON")
544 .value_parser(value_parser!(String)),
545 )
546 .arg(
547 Arg::new("directory")
548 .short('d')
549 .help("Path to directory of files. Files should end with .json")
550 .value_parser(value_parser!(String)),
551 )
552 .arg(
553 Arg::new("verbose")
554 .short('v')
555 .long("verbose")
556 .action(ArgAction::SetTrue),
557 )
558 .arg(
559 Arg::new("schema")
560 .short('s')
561 .help("Path to JSON schema file to use to validate")
562 .long("schema")
563 .value_parser(value_parser!(String)),
564 ),
565 )
566 .subcommand(
567 Command::new("extract")
568 .about(" given documents, extract embedded contents if any")
569 .arg(
570 Arg::new("agent-file")
571 .short('a')
572 .help("Path to the agent file. Otherwise use config jacs_agent_id_and_version")
573 .value_parser(value_parser!(String)),
574 )
575 .arg(
576 Arg::new("filename")
577 .short('f')
578 .help("Path to input file. Must be JSON")
579 .value_parser(value_parser!(String)),
580 )
581 .arg(
582 Arg::new("directory")
583 .short('d')
584 .help("Path to directory of files. Files should end with .json")
585 .value_parser(value_parser!(String)),
586 )
587 .arg(
588 Arg::new("verbose")
589 .short('v')
590 .long("verbose")
591 .action(ArgAction::SetTrue),
592 )
593 .arg(
594 Arg::new("schema")
595 .short('s')
596 .help("Path to JSON schema file to use to validate")
597 .long("schema")
598 .value_parser(value_parser!(String)),
599 ),
600 )
601 )
602 .subcommand(
603 Command::new("key")
604 .about("Work with JACS cryptographic keys")
605 .subcommand(
606 Command::new("reencrypt")
607 .about("Re-encrypt the private key with a new password")
608 )
609 )
610 .subcommand(
611 Command::new("mcp")
612 .about("Start the built-in JACS MCP server (stdio transport)")
613 .arg(
614 Arg::new("profile")
615 .long("profile")
616 .default_value("core")
617 .help("Tool profile: 'core' (default, core tools) or 'full' (all tools)"),
618 )
619 .subcommand(
620 Command::new("install")
621 .about("Deprecated: MCP is now built into the jacs binary")
622 .hide(true)
623 )
624 .subcommand(
625 Command::new("run")
626 .about("Deprecated: use `jacs mcp` directly")
627 .hide(true)
628 ),
629 )
630 .subcommand(
631 Command::new("a2a")
632 .about("A2A (Agent-to-Agent) trust and discovery commands")
633 .subcommand(
634 Command::new("assess")
635 .about("Assess trust level of a remote A2A Agent Card")
636 .arg(
637 Arg::new("source")
638 .required(true)
639 .help("Path to Agent Card JSON file or URL"),
640 )
641 .arg(
642 Arg::new("policy")
643 .long("policy")
644 .short('p')
645 .value_parser(["open", "verified", "strict"])
646 .default_value("verified")
647 .help("Trust policy to apply (default: verified)"),
648 )
649 .arg(
650 Arg::new("json")
651 .long("json")
652 .action(ArgAction::SetTrue)
653 .help("Output result as JSON"),
654 ),
655 )
656 .subcommand(
657 Command::new("trust")
658 .about("Add a remote A2A agent to the local trust store")
659 .arg(
660 Arg::new("source")
661 .required(true)
662 .help("Path to Agent Card JSON file or URL"),
663 ),
664 )
665 .subcommand(
666 Command::new("discover")
667 .about("Discover a remote A2A agent via its well-known Agent Card")
668 .arg(
669 Arg::new("url")
670 .required(true)
671 .help("Base URL of the agent (e.g. https://agent.example.com)"),
672 )
673 .arg(
674 Arg::new("json")
675 .long("json")
676 .action(ArgAction::SetTrue)
677 .help("Output the full Agent Card as JSON"),
678 )
679 .arg(
680 Arg::new("policy")
681 .long("policy")
682 .short('p')
683 .value_parser(["open", "verified", "strict"])
684 .default_value("verified")
685 .help("Trust policy to apply against the discovered card"),
686 ),
687 )
688 .subcommand(
689 Command::new("serve")
690 .about("Serve this agent's .well-known endpoints for A2A discovery")
691 .arg(
692 Arg::new("port")
693 .long("port")
694 .value_parser(value_parser!(u16))
695 .default_value("8080")
696 .help("Port to listen on (default: 8080)"),
697 )
698 .arg(
699 Arg::new("host")
700 .long("host")
701 .default_value("127.0.0.1")
702 .help("Host to bind to (default: 127.0.0.1)"),
703 ),
704 )
705 .subcommand(
706 Command::new("quickstart")
707 .about("Create/load an agent and start serving A2A endpoints (password required)")
708 .after_help(quickstart_password_bootstrap_help())
709 .arg(
710 Arg::new("name")
711 .long("name")
712 .value_parser(value_parser!(String))
713 .required(true)
714 .help("Agent name used for first-time quickstart creation"),
715 )
716 .arg(
717 Arg::new("domain")
718 .long("domain")
719 .value_parser(value_parser!(String))
720 .required(true)
721 .help("Agent domain used for DNS/public-key verification workflows"),
722 )
723 .arg(
724 Arg::new("description")
725 .long("description")
726 .value_parser(value_parser!(String))
727 .help("Optional human-readable agent description"),
728 )
729 .arg(
730 Arg::new("port")
731 .long("port")
732 .value_parser(value_parser!(u16))
733 .default_value("8080")
734 .help("Port to listen on (default: 8080)"),
735 )
736 .arg(
737 Arg::new("host")
738 .long("host")
739 .default_value("127.0.0.1")
740 .help("Host to bind to (default: 127.0.0.1)"),
741 )
742 .arg(
743 Arg::new("algorithm")
744 .long("algorithm")
745 .short('a')
746 .value_parser(["pq2025", "ring-Ed25519"])
747 .help("Signing algorithm (default: pq2025)"),
748 ),
749 ),
750 )
751 .subcommand(
752 Command::new("quickstart")
753 .about("Create or load a persistent agent for instant sign/verify (password required)")
754 .after_help(quickstart_password_bootstrap_help())
755 .arg(
756 Arg::new("name")
757 .long("name")
758 .value_parser(value_parser!(String))
759 .required(true)
760 .help("Agent name used for first-time quickstart creation"),
761 )
762 .arg(
763 Arg::new("domain")
764 .long("domain")
765 .value_parser(value_parser!(String))
766 .required(true)
767 .help("Agent domain used for DNS/public-key verification workflows"),
768 )
769 .arg(
770 Arg::new("description")
771 .long("description")
772 .value_parser(value_parser!(String))
773 .help("Optional human-readable agent description"),
774 )
775 .arg(
776 Arg::new("algorithm")
777 .long("algorithm")
778 .short('a')
779 .value_parser(["ed25519", "pq2025"])
780 .default_value("pq2025")
781 .help("Signing algorithm (default: pq2025)"),
782 )
783 .arg(
784 Arg::new("sign")
785 .long("sign")
786 .help("Sign JSON from stdin and print signed document to stdout")
787 .action(ArgAction::SetTrue),
788 )
789 .arg(
790 Arg::new("file")
791 .short('f')
792 .long("file")
793 .value_parser(value_parser!(String))
794 .help("Sign a JSON file instead of reading from stdin (used with --sign)"),
795 )
796 )
797 .subcommand(
798 Command::new("init")
799 .about("Initialize JACS by creating both config and agent (with keys)")
800 .arg(
801 Arg::new("yes")
802 .long("yes")
803 .short('y')
804 .action(ArgAction::SetTrue)
805 .help("Automatically set the new agent ID in jacs.config.json without prompting"),
806 )
807 )
808 .subcommand(
809 Command::new("attest")
810 .about("Create and verify attestation documents")
811 .subcommand(
812 Command::new("create")
813 .about("Create a signed attestation")
814 .arg(
815 Arg::new("subject-type")
816 .long("subject-type")
817 .value_parser(["agent", "artifact", "workflow", "identity"])
818 .help("Type of subject being attested"),
819 )
820 .arg(
821 Arg::new("subject-id")
822 .long("subject-id")
823 .value_parser(value_parser!(String))
824 .help("Identifier of the subject"),
825 )
826 .arg(
827 Arg::new("subject-digest")
828 .long("subject-digest")
829 .value_parser(value_parser!(String))
830 .help("SHA-256 digest of the subject"),
831 )
832 .arg(
833 Arg::new("claims")
834 .long("claims")
835 .value_parser(value_parser!(String))
836 .required(true)
837 .help("JSON array of claims, e.g. '[{\"name\":\"reviewed\",\"value\":true}]'"),
838 )
839 .arg(
840 Arg::new("evidence")
841 .long("evidence")
842 .value_parser(value_parser!(String))
843 .help("JSON array of evidence references"),
844 )
845 .arg(
846 Arg::new("from-document")
847 .long("from-document")
848 .value_parser(value_parser!(String))
849 .help("Lift attestation from an existing signed document file"),
850 )
851 .arg(
852 Arg::new("output")
853 .short('o')
854 .long("output")
855 .value_parser(value_parser!(String))
856 .help("Write attestation to file instead of stdout"),
857 ),
858 )
859 .subcommand(
860 Command::new("verify")
861 .about("Verify an attestation document")
862 .arg(
863 Arg::new("file")
864 .help("Path to the attestation JSON file")
865 .required(true)
866 .value_parser(value_parser!(String)),
867 )
868 .arg(
869 Arg::new("full")
870 .long("full")
871 .action(ArgAction::SetTrue)
872 .help("Use full verification (evidence + derivation chain)"),
873 )
874 .arg(
875 Arg::new("json")
876 .long("json")
877 .action(ArgAction::SetTrue)
878 .help("Output result as JSON"),
879 )
880 .arg(
881 Arg::new("key-dir")
882 .long("key-dir")
883 .value_parser(value_parser!(String))
884 .help("Directory containing public keys for verification"),
885 )
886 .arg(
887 Arg::new("max-depth")
888 .long("max-depth")
889 .value_parser(value_parser!(u32))
890 .help("Maximum derivation chain depth"),
891 ),
892 )
893 .subcommand(
894 Command::new("export-dsse")
895 .about("Export an attestation as a DSSE envelope for in-toto/SLSA")
896 .arg(
897 Arg::new("file")
898 .help("Path to the signed attestation JSON file")
899 .required(true)
900 .value_parser(value_parser!(String)),
901 )
902 .arg(
903 Arg::new("output")
904 .short('o')
905 .long("output")
906 .value_parser(value_parser!(String))
907 .help("Write DSSE envelope to file instead of stdout"),
908 ),
909 )
910 .subcommand_required(true)
911 .arg_required_else_help(true),
912 )
913 .subcommand(
914 Command::new("verify")
915 .about("Verify a signed JACS document (no agent required)")
916 .arg(
917 Arg::new("file")
918 .help("Path to the signed JACS JSON file")
919 .required_unless_present("remote")
920 .value_parser(value_parser!(String)),
921 )
922 .arg(
923 Arg::new("remote")
924 .long("remote")
925 .value_parser(value_parser!(String))
926 .help("Fetch document from URL before verifying"),
927 )
928 .arg(
929 Arg::new("json")
930 .long("json")
931 .action(ArgAction::SetTrue)
932 .help("Output result as JSON"),
933 )
934 .arg(
935 Arg::new("key-dir")
936 .long("key-dir")
937 .value_parser(value_parser!(String))
938 .help("Directory containing public keys for verification"),
939 )
940 );
941
942 #[cfg(feature = "keychain")]
944 let cmd = cmd.subcommand(
945 Command::new("keychain")
946 .about("Manage private key passwords in the OS keychain (per-agent)")
947 .subcommand(
948 Command::new("set")
949 .about("Store a password in the OS keychain for an agent")
950 .arg(
951 Arg::new("agent-id")
952 .long("agent-id")
953 .help("Agent ID to associate the password with")
954 .value_name("AGENT_ID")
955 .required(true),
956 )
957 .arg(
958 Arg::new("password")
959 .long("password")
960 .help("Password to store (if omitted, prompts interactively)")
961 .value_name("PASSWORD"),
962 ),
963 )
964 .subcommand(
965 Command::new("get")
966 .about("Retrieve the stored password for an agent (prints to stdout)")
967 .arg(
968 Arg::new("agent-id")
969 .long("agent-id")
970 .help("Agent ID to look up")
971 .value_name("AGENT_ID")
972 .required(true),
973 ),
974 )
975 .subcommand(
976 Command::new("delete")
977 .about("Remove the stored password for an agent from the OS keychain")
978 .arg(
979 Arg::new("agent-id")
980 .long("agent-id")
981 .help("Agent ID whose password to delete")
982 .value_name("AGENT_ID")
983 .required(true),
984 ),
985 )
986 .subcommand(
987 Command::new("status")
988 .about("Check if a password is stored for an agent in the OS keychain")
989 .arg(
990 Arg::new("agent-id")
991 .long("agent-id")
992 .help("Agent ID to check")
993 .value_name("AGENT_ID")
994 .required(true),
995 ),
996 )
997 .arg_required_else_help(true),
998 );
999
1000 let cmd = cmd.subcommand(
1001 Command::new("convert")
1002 .about(
1003 "Convert JACS documents between JSON, YAML, and HTML formats (no agent required)",
1004 )
1005 .arg(
1006 Arg::new("to")
1007 .long("to")
1008 .required(true)
1009 .value_parser(["json", "yaml", "html"])
1010 .help("Target format: json, yaml, or html"),
1011 )
1012 .arg(
1013 Arg::new("from")
1014 .long("from")
1015 .value_parser(["json", "yaml", "html"])
1016 .help("Source format (auto-detected from extension if omitted)"),
1017 )
1018 .arg(
1019 Arg::new("file")
1020 .short('f')
1021 .long("file")
1022 .required(true)
1023 .value_parser(value_parser!(String))
1024 .help("Input file path (use '-' for stdin)"),
1025 )
1026 .arg(
1027 Arg::new("output")
1028 .short('o')
1029 .long("output")
1030 .value_parser(value_parser!(String))
1031 .help("Output file path (defaults to stdout)"),
1032 ),
1033 );
1034
1035 cmd
1036}
1037
1038pub fn main() -> Result<(), Box<dyn Error>> {
1039 install_signal_handler();
1041
1042 let _shutdown_guard = ShutdownGuard::new();
1044 let matches = build_cli().arg_required_else_help(true).get_matches();
1045
1046 match matches.subcommand() {
1047 Some(("version", _sub_matches)) => {
1048 println!("{}", env!("CARGO_PKG_DESCRIPTION"));
1049 println!(
1050 "{} version: {}",
1051 env!("CARGO_PKG_NAME"),
1052 env!("CARGO_PKG_VERSION")
1053 );
1054 return Ok(());
1055 }
1056 Some(("config", config_matches)) => match config_matches.subcommand() {
1057 Some(("create", _create_matches)) => {
1058 handle_config_create()?;
1060 }
1061 Some(("read", _read_matches)) => {
1062 let config_path = "./jacs.config.json";
1063 match jacs::config::Config::from_file(config_path) {
1064 Ok(mut config) => {
1065 config.apply_env_overrides();
1066 println!("{}", config);
1067 }
1068 Err(e) => {
1069 eprintln!("Could not load config from '{}': {}", config_path, e);
1070 process::exit(1);
1071 }
1072 }
1073 }
1074 _ => println!("please enter subcommand see jacs config --help"),
1075 },
1076 Some(("agent", agent_matches)) => match agent_matches.subcommand() {
1077 Some(("dns", sub_m)) => {
1078 let domain = sub_m.get_one::<String>("domain").cloned();
1079 let agent_id_arg = sub_m.get_one::<String>("agent-id").cloned();
1080 let ttl = *sub_m.get_one::<u32>("ttl").unwrap();
1081 let enc = sub_m
1082 .get_one::<String>("encoding")
1083 .map(|s| s.as_str())
1084 .unwrap_or("base64");
1085 let provider = sub_m
1086 .get_one::<String>("provider")
1087 .map(|s| s.as_str())
1088 .unwrap_or("plain");
1089
1090 let _agent_file = sub_m.get_one::<String>("agent-file").cloned();
1092 let non_strict = *sub_m.get_one::<bool>("no-dns").unwrap_or(&false);
1093 let ignore_dns = *sub_m.get_one::<bool>("ignore-dns").unwrap_or(&false);
1094 let require_strict = *sub_m
1095 .get_one::<bool>("require-strict-dns")
1096 .unwrap_or(&false);
1097 let require_dns = *sub_m.get_one::<bool>("require-dns").unwrap_or(&false);
1098 let agent: Agent = load_agent_with_cli_dns_policy(
1099 ignore_dns,
1100 require_strict,
1101 require_dns,
1102 non_strict,
1103 )
1104 .expect("Failed to load agent from config");
1105 let agent_id = agent_id_arg.unwrap_or_else(|| agent.get_id().unwrap_or_default());
1106 let pk = agent.get_public_key().expect("public key");
1107 let digest = match enc {
1108 "hex" => dns_bootstrap::pubkey_digest_hex(&pk),
1109 _ => dns_bootstrap::pubkey_digest_b64(&pk),
1110 };
1111 let domain_final = domain
1112 .or_else(|| {
1113 agent
1114 .config
1115 .as_ref()
1116 .and_then(|c| c.jacs_agent_domain().clone())
1117 })
1118 .expect("domain required via --domain or jacs_agent_domain in config");
1119
1120 let rr = dns_bootstrap::build_dns_record(
1121 &domain_final,
1122 ttl,
1123 &agent_id,
1124 &digest,
1125 if enc == "hex" {
1126 dns_bootstrap::DigestEncoding::Hex
1127 } else {
1128 dns_bootstrap::DigestEncoding::Base64
1129 },
1130 );
1131
1132 println!("Plain/BIND:\n{}", dns_bootstrap::emit_plain_bind(&rr));
1133 match provider {
1134 "aws" => println!(
1135 "\nRoute53 change-batch JSON:\n{}",
1136 dns_bootstrap::emit_route53_change_batch(&rr)
1137 ),
1138 "azure" => println!(
1139 "\nAzure CLI:\n{}",
1140 dns_bootstrap::emit_azure_cli(
1141 &rr,
1142 "$RESOURCE_GROUP",
1143 &domain_final,
1144 "_v1.agent.jacs"
1145 )
1146 ),
1147 "cloudflare" => println!(
1148 "\nCloudflare curl:\n{}",
1149 dns_bootstrap::emit_cloudflare_curl(&rr, "$ZONE_ID")
1150 ),
1151 _ => {}
1152 }
1153 println!(
1154 "\nChecklist: Ensure DNSSEC is enabled for {domain} and DS is published at registrar.",
1155 domain = domain_final
1156 );
1157 }
1158 Some(("create", create_matches)) => {
1159 let filename = create_matches.get_one::<String>("filename");
1161 let create_keys = *create_matches.get_one::<bool>("create-keys").unwrap();
1162
1163 handle_agent_create(filename, create_keys)?;
1165 }
1166 Some(("verify", verify_matches)) => {
1167 let _agentfile = verify_matches.get_one::<String>("agent-file");
1168 let non_strict = *verify_matches.get_one::<bool>("no-dns").unwrap_or(&false);
1169 let require_dns = *verify_matches
1170 .get_one::<bool>("require-dns")
1171 .unwrap_or(&false);
1172 let require_strict = *verify_matches
1173 .get_one::<bool>("require-strict-dns")
1174 .unwrap_or(&false);
1175 let ignore_dns = *verify_matches
1176 .get_one::<bool>("ignore-dns")
1177 .unwrap_or(&false);
1178 let mut agent: Agent = load_agent_with_cli_dns_policy(
1179 ignore_dns,
1180 require_strict,
1181 require_dns,
1182 non_strict,
1183 )
1184 .expect("Failed to load agent from config");
1185 agent
1186 .verify_self_signature()
1187 .expect("signature verification");
1188 println!(
1189 "Agent {} signature verified OK.",
1190 agent.get_lookup_id().expect("jacsId")
1191 );
1192 }
1193 Some(("lookup", lookup_matches)) => {
1194 let domain = lookup_matches
1195 .get_one::<String>("domain")
1196 .expect("domain required");
1197 let skip_dns = *lookup_matches.get_one::<bool>("no-dns").unwrap_or(&false);
1198 let strict_dns = *lookup_matches.get_one::<bool>("strict").unwrap_or(&false);
1199
1200 println!("Agent Lookup: {}\n", domain);
1201
1202 println!("Public Key (/.well-known/jacs-pubkey.json):");
1204 let url = format!("https://{}/.well-known/jacs-pubkey.json", domain);
1205 let client = reqwest::blocking::Client::builder()
1206 .timeout(std::time::Duration::from_secs(10))
1207 .build()
1208 .expect("HTTP client");
1209 match client.get(&url).send() {
1210 Ok(response) => {
1211 if response.status().is_success() {
1212 match response.json::<serde_json::Value>() {
1213 Ok(json) => {
1214 println!(
1215 " Agent ID: {}",
1216 json.get("agentId")
1217 .and_then(|v| v.as_str())
1218 .unwrap_or("Not specified")
1219 );
1220 println!(
1221 " Algorithm: {}",
1222 json.get("algorithm")
1223 .and_then(|v| v.as_str())
1224 .unwrap_or("Not specified")
1225 );
1226 println!(
1227 " Public Key Hash: {}",
1228 json.get("publicKeyHash")
1229 .and_then(|v| v.as_str())
1230 .unwrap_or("Not specified")
1231 );
1232 if let Some(pk) = json.get("publicKey").and_then(|v| v.as_str())
1233 {
1234 let preview = if pk.len() > 60 {
1235 format!("{}...", &pk[..60])
1236 } else {
1237 pk.to_string()
1238 };
1239 println!(" Public Key: {}", preview);
1240 }
1241 }
1242 Err(e) => println!(" Error parsing response: {}", e),
1243 }
1244 } else {
1245 println!(" HTTP error: {}", response.status());
1246 }
1247 }
1248 Err(e) => println!(" Error fetching: {}", e),
1249 }
1250
1251 println!();
1252
1253 if !skip_dns {
1255 println!("DNS TXT Record (_v1.agent.jacs.{}):", domain);
1256 let owner = format!("_v1.agent.jacs.{}", domain.trim_end_matches('.'));
1257 let lookup_result = if strict_dns {
1258 dns_bootstrap::resolve_txt_dnssec(&owner)
1259 } else {
1260 dns_bootstrap::resolve_txt_insecure(&owner)
1261 };
1262 match lookup_result {
1263 Ok(txt) => {
1264 match dns_bootstrap::parse_agent_txt(&txt) {
1266 Ok(parsed) => {
1267 println!(" Version: {}", parsed.v);
1268 println!(" Agent ID: {}", parsed.jacs_agent_id);
1269 println!(" Algorithm: {:?}", parsed.alg);
1270 println!(" Encoding: {:?}", parsed.enc);
1271 println!(" Public Key Hash: {}", parsed.digest);
1272 }
1273 Err(e) => println!(" Error parsing TXT: {}", e),
1274 }
1275 println!(" Raw TXT: {}", txt);
1276 }
1277 Err(e) => {
1278 println!(" No DNS TXT record found: {}", e);
1279 if strict_dns {
1280 println!(" (Strict DNSSEC validation was required)");
1281 }
1282 }
1283 }
1284 } else {
1285 println!("DNS TXT Record: Skipped (--no-dns)");
1286 }
1287 }
1288 Some(("rotate-keys", sub_m)) => {
1289 use jacs::simple::SimpleAgent;
1290
1291 let config_path = sub_m.get_one::<String>("config").map(|s| s.as_str());
1292 let algorithm = sub_m.get_one::<String>("algorithm").map(|s| s.as_str());
1293
1294 let agent =
1295 SimpleAgent::load(config_path, None).map_err(|e| -> Box<dyn Error> {
1296 Box::new(std::io::Error::new(
1297 std::io::ErrorKind::Other,
1298 format!("Failed to load agent: {}", e),
1299 ))
1300 })?;
1301
1302 let result = jacs::simple::advanced::rotate(&agent, algorithm).map_err(
1303 |e| -> Box<dyn Error> {
1304 Box::new(std::io::Error::new(
1305 std::io::ErrorKind::Other,
1306 format!("Key rotation failed: {}", e),
1307 ))
1308 },
1309 )?;
1310
1311 println!("Key rotation successful.");
1312 println!(" Agent ID: {}", result.jacs_id);
1313 println!(" Old version: {}", result.old_version);
1314 println!(" New version: {}", result.new_version);
1315 println!(" New key hash: {}", result.new_public_key_hash);
1316 if result.transition_proof.is_some() {
1317 println!(" Transition proof: present");
1318 }
1319 }
1320 Some(("keys-list", sub_m)) => {
1321 let config_path = sub_m.get_one::<String>("config");
1322 let config_p = config_path
1323 .map(|s| s.as_str())
1324 .unwrap_or("./jacs.config.json");
1325
1326 let config =
1327 jacs::config::Config::from_file(config_p).map_err(|e| -> Box<dyn Error> {
1328 Box::new(std::io::Error::new(
1329 std::io::ErrorKind::Other,
1330 format!("Failed to load config: {}", e),
1331 ))
1332 })?;
1333
1334 let key_dir = config
1335 .jacs_key_directory()
1336 .as_deref()
1337 .unwrap_or("./jacs_keys");
1338 let algo = config
1339 .jacs_agent_key_algorithm()
1340 .as_deref()
1341 .unwrap_or("unknown");
1342 let pub_name = config
1343 .jacs_agent_public_key_filename()
1344 .as_deref()
1345 .unwrap_or("jacs.public.pem");
1346
1347 let active_path = std::path::Path::new(key_dir).join(pub_name);
1349 if active_path.exists() {
1350 let meta = std::fs::metadata(&active_path).ok();
1351 let modified = meta
1352 .and_then(|m| m.modified().ok())
1353 .map(|t| {
1354 let duration =
1355 t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
1356 format!("{}s since epoch", duration.as_secs())
1357 })
1358 .unwrap_or_else(|| "unknown".to_string());
1359 println!(
1360 "Active: {} (algorithm: {}, modified: {})",
1361 active_path.display(),
1362 algo,
1363 modified
1364 );
1365 } else {
1366 println!(
1367 "Active: (no public key found at {})",
1368 active_path.display()
1369 );
1370 }
1371
1372 let mut archived: Vec<(String, String)> = Vec::new();
1374 if let Ok(entries) = std::fs::read_dir(key_dir) {
1375 for entry in entries.flatten() {
1376 let name = entry.file_name().to_string_lossy().to_string();
1377 if name.ends_with(".pem")
1379 && name.starts_with("jacs.public.")
1380 && name != pub_name
1381 {
1382 let modified = entry
1383 .metadata()
1384 .ok()
1385 .and_then(|m| m.modified().ok())
1386 .map(|t| {
1387 let duration =
1388 t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
1389 format!("{}s since epoch", duration.as_secs())
1390 })
1391 .unwrap_or_else(|| "unknown".to_string());
1392 archived.push((name, modified));
1393 }
1394 }
1395 }
1396
1397 if archived.is_empty() {
1398 println!("Archived: (none)");
1399 } else {
1400 archived.sort_by(|a, b| b.1.cmp(&a.1));
1401 for (name, modified) in &archived {
1402 println!("Archived: {}/{} (modified: {})", key_dir, name, modified);
1403 }
1404 }
1405 }
1406 Some(("repair", sub_m)) => {
1407 use jacs::keystore::RotationJournal;
1408 use jacs::simple::SimpleAgent;
1409
1410 let config_path = sub_m.get_one::<String>("config");
1411 let config_p = config_path
1412 .map(|s| s.as_str())
1413 .unwrap_or("./jacs.config.json");
1414
1415 let config =
1416 jacs::config::Config::from_file(config_p).map_err(|e| -> Box<dyn Error> {
1417 Box::new(std::io::Error::new(
1418 std::io::ErrorKind::Other,
1419 format!("Failed to load config: {}", e),
1420 ))
1421 })?;
1422
1423 let key_dir = config
1424 .jacs_key_directory()
1425 .as_deref()
1426 .unwrap_or("./jacs_keys");
1427
1428 let journal_path = RotationJournal::journal_path(key_dir);
1429 if RotationJournal::load(&journal_path).is_some() {
1430 println!(
1431 "Incomplete rotation detected (journal at {}). Loading agent to trigger auto-repair...",
1432 journal_path
1433 );
1434 let _agent =
1436 SimpleAgent::load(Some(config_p), None).map_err(|e| -> Box<dyn Error> {
1437 Box::new(std::io::Error::new(
1438 std::io::ErrorKind::Other,
1439 format!("Repair failed: {}", e),
1440 ))
1441 })?;
1442 if RotationJournal::load(&journal_path).is_none() {
1444 println!("Config repaired successfully. Journal cleaned up.");
1445 } else {
1446 println!(
1447 "Warning: Journal still present after load. Manual intervention may be needed."
1448 );
1449 }
1450 } else {
1451 println!("No incomplete rotation detected. Nothing to repair.");
1452 }
1453 }
1454 _ => println!("please enter subcommand see jacs agent --help"),
1455 },
1456
1457 Some(("task", task_matches)) => match task_matches.subcommand() {
1458 Some(("create", create_matches)) => {
1459 let _agentfile = create_matches.get_one::<String>("agent-file");
1460 let mut agent: Agent = load_agent().expect("failed to load agent for task create");
1461 let name = create_matches
1462 .get_one::<String>("name")
1463 .expect("task name is required");
1464 let description = create_matches
1465 .get_one::<String>("description")
1466 .expect("task description is required");
1467 println!(
1468 "{}",
1469 create_task(&mut agent, name.to_string(), description.to_string()).unwrap()
1470 );
1471 }
1472 Some(("update", update_matches)) => {
1473 let mut agent: Agent = load_agent().expect("failed to load agent for task update");
1474 let task_key = update_matches
1475 .get_one::<String>("task-key")
1476 .expect("task key is required");
1477 let filename = update_matches
1478 .get_one::<String>("filename")
1479 .expect("filename is required");
1480 let updated_json = std::fs::read_to_string(filename)
1481 .unwrap_or_else(|e| panic!("Failed to read '{}': {}", filename, e));
1482 println!(
1483 "{}",
1484 jacs::update_task(&mut agent, task_key, &updated_json).unwrap()
1485 );
1486 }
1487 _ => println!("please enter subcommand see jacs task --help"),
1488 },
1489 Some(("document", document_matches)) => match document_matches.subcommand() {
1490 Some(("create", create_matches)) => {
1491 let filename = create_matches.get_one::<String>("filename");
1492 let outputfilename = create_matches.get_one::<String>("output");
1493 let directory = create_matches.get_one::<String>("directory");
1494 let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1495 let no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1496 let _agentfile = create_matches.get_one::<String>("agent-file");
1497 let schema = create_matches.get_one::<String>("schema");
1498 let attachments = create_matches
1499 .get_one::<String>("attach")
1500 .map(|s| s.as_str());
1501 let embed: Option<bool> = create_matches.get_one::<bool>("embed").copied();
1502
1503 let mut agent: Agent = load_agent().expect("REASON");
1504
1505 let _attachment_links = agent.parse_attachement_arg(attachments);
1506 let _ = create_documents(
1507 &mut agent,
1508 filename,
1509 directory,
1510 outputfilename,
1511 attachments,
1512 embed,
1513 no_save,
1514 schema,
1515 );
1516 }
1517 Some(("update", create_matches)) => {
1520 let new_filename = create_matches.get_one::<String>("new").unwrap();
1521 let original_filename = create_matches.get_one::<String>("filename").unwrap();
1522 let outputfilename = create_matches.get_one::<String>("output");
1523 let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1524 let no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1525 let _agentfile = create_matches.get_one::<String>("agent-file");
1526 let schema = create_matches.get_one::<String>("schema");
1527 let attachments = create_matches
1528 .get_one::<String>("attach")
1529 .map(|s| s.as_str());
1530 let embed: Option<bool> = create_matches.get_one::<bool>("embed").copied();
1531
1532 let mut agent: Agent = load_agent().expect("REASON");
1533
1534 let attachment_links = agent.parse_attachement_arg(attachments);
1535 update_documents(
1536 &mut agent,
1537 new_filename,
1538 original_filename,
1539 outputfilename,
1540 attachment_links,
1541 embed,
1542 no_save,
1543 schema,
1544 )?;
1545 }
1546 Some(("sign-agreement", create_matches)) => {
1547 let filename = create_matches.get_one::<String>("filename");
1548 let directory = create_matches.get_one::<String>("directory");
1549 let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1550 let _agentfile = create_matches.get_one::<String>("agent-file");
1551 let mut agent: Agent = load_agent().expect("REASON");
1552 let schema = create_matches.get_one::<String>("schema");
1553 let _no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1554
1555 sign_documents(&mut agent, schema, filename, directory)?;
1557 }
1558 Some(("check-agreement", create_matches)) => {
1559 let filename = create_matches.get_one::<String>("filename");
1560 let directory = create_matches.get_one::<String>("directory");
1561 let _agentfile = create_matches.get_one::<String>("agent-file");
1562 let mut agent: Agent = load_agent().expect("REASON");
1563 let schema = create_matches.get_one::<String>("schema");
1564
1565 let _files: Vec<String> = default_set_file_list(filename, directory, None)
1567 .expect("Failed to determine file list");
1568 check_agreement(&mut agent, schema, filename, directory)?;
1569 }
1570 Some(("create-agreement", create_matches)) => {
1571 let filename = create_matches.get_one::<String>("filename");
1572 let directory = create_matches.get_one::<String>("directory");
1573 let _verbose = *create_matches.get_one::<bool>("verbose").unwrap_or(&false);
1574 let _agentfile = create_matches.get_one::<String>("agent-file");
1575
1576 let schema = create_matches.get_one::<String>("schema");
1577 let no_save = *create_matches.get_one::<bool>("no-save").unwrap_or(&false);
1578 let agentids: Vec<String> = create_matches .get_many::<String>("agentids")
1580 .unwrap_or_default()
1581 .map(|s| s.to_string())
1582 .collect();
1583
1584 let mut agent: Agent = load_agent().expect("REASON");
1585 let _ =
1587 create_agreement(&mut agent, agentids, filename, schema, no_save, directory);
1588 }
1589
1590 Some(("verify", verify_matches)) => {
1591 let filename = verify_matches.get_one::<String>("filename");
1592 let directory = verify_matches.get_one::<String>("directory");
1593 let _verbose = *verify_matches.get_one::<bool>("verbose").unwrap_or(&false);
1594 let _agentfile = verify_matches.get_one::<String>("agent-file");
1595 let mut agent: Agent = load_agent().expect("REASON");
1596 let schema = verify_matches.get_one::<String>("schema");
1597 verify_documents(&mut agent, schema, filename, directory)?;
1599 }
1600
1601 Some(("extract", extract_matches)) => {
1602 let filename = extract_matches.get_one::<String>("filename");
1603 let directory = extract_matches.get_one::<String>("directory");
1604 let _verbose = *extract_matches.get_one::<bool>("verbose").unwrap_or(&false);
1605 let _agentfile = extract_matches.get_one::<String>("agent-file");
1606 let mut agent: Agent = load_agent().expect("REASON");
1607 let schema = extract_matches.get_one::<String>("schema");
1608 let _files: Vec<String> = default_set_file_list(filename, directory, None)
1610 .expect("Failed to determine file list");
1611 extract_documents(&mut agent, schema, filename, directory)?;
1613 }
1614
1615 _ => println!("please enter subcommand see jacs document --help"),
1616 },
1617 Some(("key", key_matches)) => match key_matches.subcommand() {
1618 Some(("reencrypt", _reencrypt_matches)) => {
1619 use jacs::crypt::aes_encrypt::password_requirements;
1620 use jacs::simple::SimpleAgent;
1621
1622 let agent = SimpleAgent::load(None, None).map_err(|e| -> Box<dyn Error> {
1624 Box::new(std::io::Error::new(
1625 std::io::ErrorKind::Other,
1626 format!("Failed to load agent: {}", e),
1627 ))
1628 })?;
1629
1630 println!("Re-encrypting private key.\n");
1631
1632 println!("Enter current password:");
1634 let old_password = read_password().map_err(|e| -> Box<dyn Error> {
1635 Box::new(std::io::Error::new(
1636 std::io::ErrorKind::Other,
1637 format!("Error reading password: {}", e),
1638 ))
1639 })?;
1640
1641 if old_password.is_empty() {
1642 eprintln!("Error: current password cannot be empty.");
1643 process::exit(1);
1644 }
1645
1646 println!("\n{}", password_requirements());
1648 println!("\nEnter new password:");
1649 let new_password = read_password().map_err(|e| -> Box<dyn Error> {
1650 Box::new(std::io::Error::new(
1651 std::io::ErrorKind::Other,
1652 format!("Error reading password: {}", e),
1653 ))
1654 })?;
1655
1656 println!("Confirm new password:");
1657 let new_password_confirm = read_password().map_err(|e| -> Box<dyn Error> {
1658 Box::new(std::io::Error::new(
1659 std::io::ErrorKind::Other,
1660 format!("Error reading password: {}", e),
1661 ))
1662 })?;
1663
1664 if new_password != new_password_confirm {
1665 eprintln!("Error: new passwords do not match.");
1666 process::exit(1);
1667 }
1668
1669 jacs::simple::advanced::reencrypt_key(&agent, &old_password, &new_password)
1670 .map_err(|e| -> Box<dyn Error> {
1671 Box::new(std::io::Error::new(
1672 std::io::ErrorKind::Other,
1673 format!("Re-encryption failed: {}", e),
1674 ))
1675 })?;
1676
1677 println!("Private key re-encrypted successfully.");
1678 }
1679 _ => println!("please enter subcommand see jacs key --help"),
1680 },
1681 #[cfg(feature = "mcp")]
1682 Some(("mcp", mcp_matches)) => match mcp_matches.subcommand() {
1683 Some(("install", _)) => {
1684 eprintln!("`jacs mcp install` is no longer needed.");
1685 eprintln!("MCP is built into the jacs binary. Use `jacs mcp` to serve.");
1686 process::exit(0);
1687 }
1688 Some(("run", _)) => {
1689 eprintln!("`jacs mcp run` is no longer needed.");
1690 eprintln!("Use `jacs mcp` directly to start the MCP server.");
1691 process::exit(0);
1692 }
1693 _ => {
1694 let profile_str = mcp_matches.get_one::<String>("profile").map(|s| s.as_str());
1695 let profile = jacs_mcp::Profile::resolve(profile_str);
1696 let (agent, info) = jacs_mcp::load_agent_from_config_env_with_info()?;
1697 let state_roots = info["data_directory"]
1698 .as_str()
1699 .map(std::path::PathBuf::from)
1700 .into_iter()
1701 .collect();
1702 let server = jacs_mcp::JacsMcpServer::with_profile_and_state_roots(
1703 agent,
1704 profile,
1705 state_roots,
1706 );
1707 let rt = tokio::runtime::Runtime::new()?;
1708 rt.block_on(jacs_mcp::serve_stdio(server))?;
1709 }
1710 },
1711 #[cfg(not(feature = "mcp"))]
1712 Some(("mcp", _)) => {
1713 eprintln!(
1714 "MCP support not compiled. Install with default features: cargo install jacs-cli"
1715 );
1716 process::exit(1);
1717 }
1718 Some(("a2a", a2a_matches)) => match a2a_matches.subcommand() {
1719 Some(("assess", assess_matches)) => {
1720 use jacs::a2a::AgentCard;
1721 use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
1722
1723 let source = assess_matches.get_one::<String>("source").unwrap();
1724 let policy_str = assess_matches
1725 .get_one::<String>("policy")
1726 .map(|s| s.as_str())
1727 .unwrap_or("verified");
1728 let json_output = *assess_matches.get_one::<bool>("json").unwrap_or(&false);
1729
1730 let policy = A2ATrustPolicy::from_str_loose(policy_str)
1731 .map_err(|e| Box::<dyn Error>::from(format!("Invalid policy: {}", e)))?;
1732
1733 let card_json = if source.starts_with("http://") || source.starts_with("https://") {
1735 let client = reqwest::blocking::Client::builder()
1736 .timeout(std::time::Duration::from_secs(10))
1737 .build()
1738 .map_err(|e| format!("HTTP client error: {}", e))?;
1739 client
1740 .get(source.as_str())
1741 .send()
1742 .map_err(|e| format!("Fetch failed: {}", e))?
1743 .text()
1744 .map_err(|e| format!("Read body failed: {}", e))?
1745 } else {
1746 std::fs::read_to_string(source)
1747 .map_err(|e| format!("Read file failed: {}", e))?
1748 };
1749
1750 let card: AgentCard = serde_json::from_str(&card_json)
1751 .map_err(|e| format!("Invalid Agent Card JSON: {}", e))?;
1752
1753 let agent = jacs::get_empty_agent();
1755 let assessment = assess_a2a_agent(&agent, &card, policy);
1756
1757 if json_output {
1758 println!(
1759 "{}",
1760 serde_json::to_string_pretty(&assessment)
1761 .expect("assessment serialization")
1762 );
1763 } else {
1764 println!("Agent: {}", card.name);
1765 println!(
1766 "Agent ID: {}",
1767 assessment.agent_id.as_deref().unwrap_or("(not specified)")
1768 );
1769 println!("Policy: {}", assessment.policy);
1770 println!("Trust Level: {}", assessment.trust_level);
1771 println!(
1772 "Allowed: {}",
1773 if assessment.allowed { "YES" } else { "NO" }
1774 );
1775 println!("JACS Ext: {}", assessment.jacs_registered);
1776 println!("Reason: {}", assessment.reason);
1777 if !assessment.allowed {
1778 process::exit(1);
1779 }
1780 }
1781 }
1782 Some(("trust", trust_matches)) => {
1783 use jacs::a2a::AgentCard;
1784 use jacs::trust;
1785
1786 let source = trust_matches.get_one::<String>("source").unwrap();
1787
1788 let card_json = if source.starts_with("http://") || source.starts_with("https://") {
1790 let client = reqwest::blocking::Client::builder()
1791 .timeout(std::time::Duration::from_secs(10))
1792 .build()
1793 .map_err(|e| format!("HTTP client error: {}", e))?;
1794 client
1795 .get(source.as_str())
1796 .send()
1797 .map_err(|e| format!("Fetch failed: {}", e))?
1798 .text()
1799 .map_err(|e| format!("Read body failed: {}", e))?
1800 } else {
1801 std::fs::read_to_string(source)
1802 .map_err(|e| format!("Read file failed: {}", e))?
1803 };
1804
1805 let card: AgentCard = serde_json::from_str(&card_json)
1806 .map_err(|e| format!("Invalid Agent Card JSON: {}", e))?;
1807
1808 let agent_id = card
1810 .metadata
1811 .as_ref()
1812 .and_then(|m| m.get("jacsId"))
1813 .and_then(|v| v.as_str())
1814 .ok_or("Agent Card has no jacsId in metadata")?;
1815 let agent_version = card
1816 .metadata
1817 .as_ref()
1818 .and_then(|m| m.get("jacsVersion"))
1819 .and_then(|v| v.as_str())
1820 .unwrap_or("unknown");
1821
1822 let key = format!("{}:{}", agent_id, agent_version);
1823
1824 trust::trust_a2a_card(&key, &card_json)?;
1828
1829 println!(
1830 "Saved unverified A2A Agent Card bookmark '{}' ({})",
1831 card.name, agent_id
1832 );
1833 println!(" Version: {}", agent_version);
1834 println!(" Bookmark key: {}", key);
1835 println!(
1836 " This entry is not cryptographically trusted until verified JACS identity material is added."
1837 );
1838 }
1839 Some(("discover", discover_matches)) => {
1840 use jacs::a2a::AgentCard;
1841 use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
1842
1843 let base_url = discover_matches.get_one::<String>("url").unwrap();
1844 let json_output = *discover_matches.get_one::<bool>("json").unwrap_or(&false);
1845 let policy_str = discover_matches
1846 .get_one::<String>("policy")
1847 .map(|s| s.as_str())
1848 .unwrap_or("verified");
1849
1850 let policy = A2ATrustPolicy::from_str_loose(policy_str)
1851 .map_err(|e| Box::<dyn Error>::from(format!("Invalid policy: {}", e)))?;
1852
1853 let trimmed = base_url.trim_end_matches('/');
1855 let card_url = format!("{}/.well-known/agent-card.json", trimmed);
1856
1857 let client = reqwest::blocking::Client::builder()
1858 .timeout(std::time::Duration::from_secs(10))
1859 .build()
1860 .map_err(|e| format!("HTTP client error: {}", e))?;
1861
1862 let response = client
1863 .get(&card_url)
1864 .send()
1865 .map_err(|e| format!("Failed to fetch {}: {}", card_url, e))?;
1866
1867 if !response.status().is_success() {
1868 eprintln!(
1869 "Failed to discover agent at {}: HTTP {}",
1870 card_url,
1871 response.status()
1872 );
1873 process::exit(1);
1874 }
1875
1876 let card_json = response
1877 .text()
1878 .map_err(|e| format!("Read body failed: {}", e))?;
1879
1880 let card: AgentCard = serde_json::from_str(&card_json)
1881 .map_err(|e| format!("Invalid Agent Card JSON at {}: {}", card_url, e))?;
1882
1883 if json_output {
1884 println!(
1886 "{}",
1887 serde_json::to_string_pretty(&card).expect("card serialization")
1888 );
1889 } else {
1890 println!("Discovered A2A Agent: {}", card.name);
1892 println!(" Description: {}", card.description);
1893 println!(" Version: {}", card.version);
1894 println!(" Protocol: {}", card.protocol_versions.join(", "));
1895
1896 for iface in &card.supported_interfaces {
1898 println!(" Endpoint: {} ({})", iface.url, iface.protocol_binding);
1899 }
1900
1901 if !card.skills.is_empty() {
1903 println!(" Skills:");
1904 for skill in &card.skills {
1905 println!(" - {} ({})", skill.name, skill.id);
1906 }
1907 }
1908
1909 let has_jacs = card
1911 .capabilities
1912 .extensions
1913 .as_ref()
1914 .map(|exts| exts.iter().any(|e| e.uri == jacs::a2a::JACS_EXTENSION_URI))
1915 .unwrap_or(false);
1916 println!(" JACS: {}", if has_jacs { "YES" } else { "NO" });
1917
1918 let agent = jacs::get_empty_agent();
1920 let assessment = assess_a2a_agent(&agent, &card, policy);
1921 println!(
1922 " Trust: {} ({})",
1923 assessment.trust_level, assessment.reason
1924 );
1925 if !assessment.allowed {
1926 println!(
1927 " WARNING: Agent not allowed under '{}' policy",
1928 policy_str
1929 );
1930 }
1931 }
1932 }
1933 Some(("serve", serve_matches)) => {
1934 let port = *serve_matches.get_one::<u16>("port").unwrap();
1935 let host = serve_matches
1936 .get_one::<String>("host")
1937 .map(|s| s.as_str())
1938 .unwrap_or("127.0.0.1");
1939
1940 ensure_cli_private_key_password().map_err(|e| -> Box<dyn Error> {
1942 Box::new(std::io::Error::other(format!(
1943 "Password bootstrap failed: {}\n\n{}",
1944 e,
1945 quickstart_password_bootstrap_help()
1946 )))
1947 })?;
1948 let (agent, info) = jacs::simple::advanced::quickstart(
1949 "jacs-agent",
1950 "localhost",
1951 Some("JACS A2A agent"),
1952 None,
1953 None,
1954 )
1955 .map_err(|e| wrap_quickstart_error_with_password_help("Failed to load agent", e))?;
1956
1957 let agent_card = jacs::a2a::simple::export_agent_card(&agent).map_err(
1959 |e| -> Box<dyn Error> {
1960 Box::new(std::io::Error::new(
1961 std::io::ErrorKind::Other,
1962 format!("Failed to export Agent Card: {}", e),
1963 ))
1964 },
1965 )?;
1966
1967 let documents = jacs::a2a::simple::generate_well_known_documents(&agent, None)
1969 .map_err(|e| -> Box<dyn Error> {
1970 Box::new(std::io::Error::new(
1971 std::io::ErrorKind::Other,
1972 format!("Failed to generate well-known documents: {}", e),
1973 ))
1974 })?;
1975
1976 let mut routes: std::collections::HashMap<String, String> =
1978 std::collections::HashMap::new();
1979 for (path, value) in &documents {
1980 routes.insert(
1981 path.clone(),
1982 serde_json::to_string_pretty(value).unwrap_or_default(),
1983 );
1984 }
1985
1986 let addr = format!("{}:{}", host, port);
1987 let server = tiny_http::Server::http(&addr)
1988 .map_err(|e| format!("Failed to start server on {}: {}", addr, e))?;
1989
1990 println!("Serving A2A well-known endpoints at http://{}", addr);
1991 println!(" Agent: {} ({})", agent_card.name, info.agent_id);
1992 println!(" Endpoints:");
1993 for path in routes.keys() {
1994 println!(" http://{}{}", addr, path);
1995 }
1996 println!("\nPress Ctrl+C to stop.");
1997
1998 for request in server.incoming_requests() {
1999 let url = request.url().to_string();
2000 if let Some(body) = routes.get(&url) {
2001 let response = tiny_http::Response::from_string(body.clone()).with_header(
2002 tiny_http::Header::from_bytes(
2003 &b"Content-Type"[..],
2004 &b"application/json"[..],
2005 )
2006 .unwrap(),
2007 );
2008 let _ = request.respond(response);
2009 } else {
2010 let response =
2011 tiny_http::Response::from_string("{\"error\": \"not found\"}")
2012 .with_status_code(404)
2013 .with_header(
2014 tiny_http::Header::from_bytes(
2015 &b"Content-Type"[..],
2016 &b"application/json"[..],
2017 )
2018 .unwrap(),
2019 );
2020 let _ = request.respond(response);
2021 }
2022 }
2023 }
2024 Some(("quickstart", qs_matches)) => {
2025 let port = *qs_matches.get_one::<u16>("port").unwrap();
2026 let host = qs_matches
2027 .get_one::<String>("host")
2028 .map(|s| s.as_str())
2029 .unwrap_or("127.0.0.1");
2030 let algorithm = qs_matches
2031 .get_one::<String>("algorithm")
2032 .map(|s| s.as_str());
2033 let name = qs_matches
2034 .get_one::<String>("name")
2035 .map(|s| s.as_str())
2036 .unwrap_or("jacs-agent");
2037 let domain = qs_matches
2038 .get_one::<String>("domain")
2039 .map(|s| s.as_str())
2040 .unwrap_or("localhost");
2041 let description = qs_matches
2042 .get_one::<String>("description")
2043 .map(|s| s.as_str());
2044
2045 ensure_cli_private_key_password().map_err(|e| -> Box<dyn Error> {
2047 Box::new(std::io::Error::other(format!(
2048 "Password bootstrap failed: {}\n\n{}",
2049 e,
2050 quickstart_password_bootstrap_help()
2051 )))
2052 })?;
2053 let (agent, info) =
2054 jacs::simple::advanced::quickstart(name, domain, description, algorithm, None)
2055 .map_err(|e| {
2056 wrap_quickstart_error_with_password_help(
2057 "Failed to quickstart agent",
2058 e,
2059 )
2060 })?;
2061
2062 let agent_card = jacs::a2a::simple::export_agent_card(&agent).map_err(
2064 |e| -> Box<dyn Error> {
2065 Box::new(std::io::Error::new(
2066 std::io::ErrorKind::Other,
2067 format!("Failed to export Agent Card: {}", e),
2068 ))
2069 },
2070 )?;
2071
2072 let documents = jacs::a2a::simple::generate_well_known_documents(&agent, None)
2074 .map_err(|e| -> Box<dyn Error> {
2075 Box::new(std::io::Error::new(
2076 std::io::ErrorKind::Other,
2077 format!("Failed to generate well-known documents: {}", e),
2078 ))
2079 })?;
2080
2081 let mut routes: std::collections::HashMap<String, String> =
2083 std::collections::HashMap::new();
2084 for (path, value) in &documents {
2085 routes.insert(
2086 path.clone(),
2087 serde_json::to_string_pretty(value).unwrap_or_default(),
2088 );
2089 }
2090
2091 let addr = format!("{}:{}", host, port);
2092 let server = tiny_http::Server::http(&addr)
2093 .map_err(|e| format!("Failed to start server on {}: {}", addr, e))?;
2094
2095 println!("A2A Quickstart");
2096 println!("==============");
2097 println!("Agent: {} ({})", agent_card.name, info.agent_id);
2098 println!("Algorithm: {}", algorithm.unwrap_or("pq2025"));
2099 println!();
2100 println!("Discovery URL: http://{}/.well-known/agent-card.json", addr);
2101 println!();
2102 println!("Endpoints:");
2103 for path in routes.keys() {
2104 println!(" http://{}{}", addr, path);
2105 }
2106 println!();
2107 println!("Press Ctrl+C to stop.");
2108
2109 for request in server.incoming_requests() {
2110 let url = request.url().to_string();
2111 if let Some(body) = routes.get(&url) {
2112 let response = tiny_http::Response::from_string(body.clone()).with_header(
2113 tiny_http::Header::from_bytes(
2114 &b"Content-Type"[..],
2115 &b"application/json"[..],
2116 )
2117 .unwrap(),
2118 );
2119 let _ = request.respond(response);
2120 } else {
2121 let response =
2122 tiny_http::Response::from_string("{\"error\": \"not found\"}")
2123 .with_status_code(404)
2124 .with_header(
2125 tiny_http::Header::from_bytes(
2126 &b"Content-Type"[..],
2127 &b"application/json"[..],
2128 )
2129 .unwrap(),
2130 );
2131 let _ = request.respond(response);
2132 }
2133 }
2134 }
2135 _ => println!("please enter subcommand see jacs a2a --help"),
2136 },
2137 Some(("quickstart", qs_matches)) => {
2138 let algorithm = qs_matches
2139 .get_one::<String>("algorithm")
2140 .map(|s| s.as_str());
2141 let name = qs_matches
2142 .get_one::<String>("name")
2143 .map(|s| s.as_str())
2144 .unwrap_or("jacs-agent");
2145 let domain = qs_matches
2146 .get_one::<String>("domain")
2147 .map(|s| s.as_str())
2148 .unwrap_or("localhost");
2149 let description = qs_matches
2150 .get_one::<String>("description")
2151 .map(|s| s.as_str());
2152 let do_sign = *qs_matches.get_one::<bool>("sign").unwrap_or(&false);
2153 let sign_file = qs_matches.get_one::<String>("file");
2154
2155 if let Err(e) = ensure_cli_private_key_password() {
2158 eprintln!("Note: {}", e);
2159 }
2160
2161 if env::var("JACS_PRIVATE_KEY_PASSWORD")
2163 .unwrap_or_default()
2164 .trim()
2165 .is_empty()
2166 {
2167 eprintln!("{}", jacs::crypt::aes_encrypt::password_requirements());
2168 let password = loop {
2169 eprintln!("Enter a password for your JACS private key:");
2170 let pw =
2171 read_password().map_err(|e| format!("Failed to read password: {}", e))?;
2172 if pw.trim().is_empty() {
2173 eprintln!("Password cannot be empty. Please try again.");
2174 continue;
2175 }
2176 eprintln!("Confirm password:");
2177 let pw2 =
2178 read_password().map_err(|e| format!("Failed to read password: {}", e))?;
2179 if pw != pw2 {
2180 eprintln!("Passwords do not match. Please try again.");
2181 continue;
2182 }
2183 break pw;
2184 };
2185
2186 unsafe {
2188 env::set_var("JACS_PRIVATE_KEY_PASSWORD", &password);
2189 }
2190
2191 }
2194
2195 let (agent, info) =
2196 jacs::simple::advanced::quickstart(name, domain, description, algorithm, None)
2197 .map_err(|e| {
2198 wrap_quickstart_error_with_password_help("Quickstart failed", e)
2199 })?;
2200
2201 if do_sign {
2202 let input = if let Some(file_path) = sign_file {
2204 std::fs::read_to_string(file_path)?
2205 } else {
2206 use std::io::Read;
2207 let mut buf = String::new();
2208 std::io::stdin().read_to_string(&mut buf)?;
2209 buf
2210 };
2211
2212 let value: serde_json::Value = serde_json::from_str(&input)
2213 .map_err(|e| format!("Invalid JSON input: {}", e))?;
2214
2215 let signed = agent.sign_message(&value).map_err(|e| -> Box<dyn Error> {
2216 Box::new(std::io::Error::new(
2217 std::io::ErrorKind::Other,
2218 format!("Signing failed: {}", e),
2219 ))
2220 })?;
2221
2222 println!("{}", signed.raw);
2223 } else {
2224 println!("JACS agent ready ({})", info.algorithm);
2226 println!(" Agent ID: {}", info.agent_id);
2227 println!(" Version: {}", info.version);
2228 println!(" Config: {}", info.config_path);
2229 println!(" Keys: {}", info.key_directory);
2230 println!();
2231 println!("Sign something:");
2232 println!(" echo '{{\"hello\":\"world\"}}' | jacs quickstart --sign");
2233 }
2234 }
2235 #[cfg(feature = "attestation")]
2236 Some(("attest", attest_matches)) => {
2237 use jacs::attestation::types::*;
2238 use jacs::simple::SimpleAgent;
2239
2240 match attest_matches.subcommand() {
2241 Some(("create", create_matches)) => {
2242 ensure_cli_private_key_password()?;
2244
2245 let agent = match SimpleAgent::load(None, None) {
2247 Ok(a) => a,
2248 Err(e) => {
2249 eprintln!("Failed to load agent: {}", e);
2250 eprintln!("Run `jacs quickstart` first to create an agent.");
2251 process::exit(1);
2252 }
2253 };
2254
2255 let claims_str = create_matches
2257 .get_one::<String>("claims")
2258 .expect("claims is required");
2259 let claims: Vec<Claim> = serde_json::from_str(claims_str).map_err(|e| {
2260 format!(
2261 "Invalid claims JSON: {}. \
2262 Provide a JSON array like '[{{\"name\":\"reviewed\",\"value\":true}}]'",
2263 e
2264 )
2265 })?;
2266
2267 let evidence: Vec<EvidenceRef> =
2269 if let Some(ev_str) = create_matches.get_one::<String>("evidence") {
2270 serde_json::from_str(ev_str)
2271 .map_err(|e| format!("Invalid evidence JSON: {}", e))?
2272 } else {
2273 vec![]
2274 };
2275
2276 let att_json = if let Some(doc_path) =
2277 create_matches.get_one::<String>("from-document")
2278 {
2279 let doc_content = std::fs::read_to_string(doc_path).map_err(|e| {
2281 format!("Failed to read document '{}': {}", doc_path, e)
2282 })?;
2283 let result = jacs::attestation::simple::lift(&agent, &doc_content, &claims)
2284 .map_err(|e| {
2285 format!("Failed to lift document to attestation: {}", e)
2286 })?;
2287 result.raw
2288 } else {
2289 let subject_type_str = create_matches
2291 .get_one::<String>("subject-type")
2292 .ok_or("--subject-type is required when not using --from-document")?;
2293 let subject_id = create_matches
2294 .get_one::<String>("subject-id")
2295 .ok_or("--subject-id is required when not using --from-document")?;
2296 let subject_digest = create_matches
2297 .get_one::<String>("subject-digest")
2298 .ok_or("--subject-digest is required when not using --from-document")?;
2299
2300 let subject_type = match subject_type_str.as_str() {
2301 "agent" => SubjectType::Agent,
2302 "artifact" => SubjectType::Artifact,
2303 "workflow" => SubjectType::Workflow,
2304 "identity" => SubjectType::Identity,
2305 other => {
2306 return Err(format!("Unknown subject type: '{}'", other).into());
2307 }
2308 };
2309
2310 let subject = AttestationSubject {
2311 subject_type,
2312 id: subject_id.clone(),
2313 digests: DigestSet {
2314 sha256: subject_digest.clone(),
2315 sha512: None,
2316 additional: std::collections::HashMap::new(),
2317 },
2318 };
2319
2320 let result = jacs::attestation::simple::create(
2321 &agent, &subject, &claims, &evidence, None, None,
2322 )
2323 .map_err(|e| format!("Failed to create attestation: {}", e))?;
2324 result.raw
2325 };
2326
2327 if let Some(output_path) = create_matches.get_one::<String>("output") {
2329 std::fs::write(output_path, &att_json).map_err(|e| {
2330 format!("Failed to write output file '{}': {}", output_path, e)
2331 })?;
2332 eprintln!("Attestation written to {}", output_path);
2333 } else {
2334 println!("{}", att_json);
2335 }
2336 }
2337 Some(("verify", verify_matches)) => {
2338 let file_path = verify_matches
2339 .get_one::<String>("file")
2340 .expect("file is required");
2341 let full = *verify_matches.get_one::<bool>("full").unwrap_or(&false);
2342 let json_output = *verify_matches.get_one::<bool>("json").unwrap_or(&false);
2343 let key_dir = verify_matches.get_one::<String>("key-dir");
2344 let max_depth = verify_matches.get_one::<u32>("max-depth");
2345
2346 if let Some(kd) = key_dir {
2348 unsafe { std::env::set_var("JACS_KEY_DIRECTORY", kd) };
2350 }
2351
2352 if let Some(depth) = max_depth {
2354 unsafe {
2356 std::env::set_var("JACS_MAX_DERIVATION_DEPTH", depth.to_string())
2357 };
2358 }
2359
2360 let att_content = std::fs::read_to_string(file_path).map_err(|e| {
2362 format!("Failed to read attestation file '{}': {}", file_path, e)
2363 })?;
2364
2365 ensure_cli_private_key_password().ok();
2367 let agent = match SimpleAgent::load(None, None) {
2368 Ok(a) => a,
2369 Err(_) => {
2370 let (a, _) = SimpleAgent::ephemeral(Some("ed25519"))
2371 .map_err(|e| format!("Failed to create verifier: {}", e))?;
2372 a
2373 }
2374 };
2375
2376 let att_value: serde_json::Value = serde_json::from_str(&att_content)
2378 .map_err(|e| format!("Invalid attestation JSON: {}", e))?;
2379 let doc_key = format!(
2380 "{}:{}",
2381 att_value["jacsId"].as_str().unwrap_or("unknown"),
2382 att_value["jacsVersion"].as_str().unwrap_or("unknown")
2383 );
2384
2385 let verify_result = agent.verify(&att_content);
2388 if let Err(e) = &verify_result {
2389 if json_output {
2390 let out = serde_json::json!({
2391 "valid": false,
2392 "error": e.to_string(),
2393 });
2394 println!("{}", serde_json::to_string_pretty(&out).unwrap());
2395 } else {
2396 eprintln!("Verification error: {}", e);
2397 }
2398 process::exit(1);
2399 }
2400
2401 let att_result = if full {
2403 jacs::attestation::simple::verify_full(&agent, &doc_key)
2404 } else {
2405 jacs::attestation::simple::verify(&agent, &doc_key)
2406 };
2407
2408 match att_result {
2409 Ok(r) => {
2410 if json_output {
2411 println!("{}", serde_json::to_string_pretty(&r).unwrap());
2412 } else {
2413 println!(
2414 "Status: {}",
2415 if r.valid { "VALID" } else { "INVALID" }
2416 );
2417 println!(
2418 "Signature: {}",
2419 if r.crypto.signature_valid {
2420 "valid"
2421 } else {
2422 "INVALID"
2423 }
2424 );
2425 println!(
2426 "Hash: {}",
2427 if r.crypto.hash_valid {
2428 "valid"
2429 } else {
2430 "INVALID"
2431 }
2432 );
2433 if !r.crypto.signer_id.is_empty() {
2434 println!("Signer: {}", r.crypto.signer_id);
2435 }
2436 if !r.evidence.is_empty() {
2437 println!("Evidence: {} items checked", r.evidence.len());
2438 }
2439 if !r.errors.is_empty() {
2440 for err in &r.errors {
2441 eprintln!(" Error: {}", err);
2442 }
2443 }
2444 }
2445 if !r.valid {
2446 process::exit(1);
2447 }
2448 }
2449 Err(e) => {
2450 if json_output {
2451 let out = serde_json::json!({
2452 "valid": false,
2453 "error": e.to_string(),
2454 });
2455 println!("{}", serde_json::to_string_pretty(&out).unwrap());
2456 } else {
2457 eprintln!("Attestation verification error: {}", e);
2458 }
2459 process::exit(1);
2460 }
2461 }
2462 }
2463 Some(("export-dsse", export_matches)) => {
2464 let file_path = export_matches
2465 .get_one::<String>("file")
2466 .expect("file argument required");
2467 let output_path = export_matches.get_one::<String>("output");
2468
2469 let attestation_json = std::fs::read_to_string(file_path).unwrap_or_else(|e| {
2470 eprintln!("Cannot read {}: {}", file_path, e);
2471 process::exit(1);
2472 });
2473
2474 let (_agent, _info) = SimpleAgent::ephemeral(Some("ring-Ed25519"))
2475 .unwrap_or_else(|e| {
2476 eprintln!("Failed to create agent: {}", e);
2477 process::exit(1);
2478 });
2479
2480 match jacs::attestation::simple::export_dsse(&attestation_json) {
2481 Ok(envelope_json) => {
2482 if let Some(out_path) = output_path {
2483 std::fs::write(out_path, &envelope_json).unwrap_or_else(|e| {
2484 eprintln!("Cannot write to {}: {}", out_path, e);
2485 process::exit(1);
2486 });
2487 println!("DSSE envelope written to {}", out_path);
2488 } else {
2489 println!("{}", envelope_json);
2490 }
2491 }
2492 Err(e) => {
2493 eprintln!("Failed to export DSSE envelope: {}", e);
2494 process::exit(1);
2495 }
2496 }
2497 }
2498 _ => {
2499 eprintln!(
2500 "Use 'jacs attest create', 'jacs attest verify', or 'jacs attest export-dsse'. See --help."
2501 );
2502 process::exit(1);
2503 }
2504 }
2505 }
2506 Some(("verify", verify_matches)) => {
2507 use jacs::simple::SimpleAgent;
2508 use serde_json::json;
2509
2510 let file_path = verify_matches.get_one::<String>("file");
2511 let remote_url = verify_matches.get_one::<String>("remote");
2512 let json_output = *verify_matches.get_one::<bool>("json").unwrap_or(&false);
2513 let key_dir = verify_matches.get_one::<String>("key-dir");
2514
2515 if let Some(kd) = key_dir {
2517 unsafe { std::env::set_var("JACS_KEY_DIRECTORY", kd) };
2519 }
2520
2521 let document = if let Some(url) = remote_url {
2523 let client = reqwest::blocking::Client::builder()
2524 .timeout(std::time::Duration::from_secs(30))
2525 .build()
2526 .map_err(|e| format!("HTTP client error: {}", e))?;
2527 let resp = client
2528 .get(url)
2529 .send()
2530 .map_err(|e| format!("Fetch failed: {}", e))?;
2531 if !resp.status().is_success() {
2532 eprintln!("HTTP error: {}", resp.status());
2533 process::exit(1);
2534 }
2535 resp.text()
2536 .map_err(|e| format!("Read body failed: {}", e))?
2537 } else if let Some(path) = file_path {
2538 std::fs::read_to_string(path).map_err(|e| format!("Read file failed: {}", e))?
2539 } else {
2540 eprintln!("Provide a file path or --remote <url>");
2541 process::exit(1);
2542 };
2543
2544 let agent = if std::path::Path::new("./jacs.config.json").exists() {
2548 if let Err(e) = ensure_cli_private_key_password() {
2549 eprintln!("Warning: Password bootstrap failed: {}", e);
2550 eprintln!("{}", quickstart_password_bootstrap_help());
2551 }
2552 match SimpleAgent::load(None, None) {
2553 Ok(a) => a,
2554 Err(e) => {
2555 let lower = e.to_string().to_lowercase();
2556 if lower.contains("password")
2557 || lower.contains("decrypt")
2558 || lower.contains("private key")
2559 {
2560 eprintln!(
2561 "Warning: Could not load local agent from ./jacs.config.json: {}",
2562 e
2563 );
2564 eprintln!("{}", quickstart_password_bootstrap_help());
2565 }
2566 let (a, _) = SimpleAgent::ephemeral(Some("ed25519"))
2567 .map_err(|e| format!("Failed to create verifier: {}", e))?;
2568 a
2569 }
2570 }
2571 } else {
2572 let (a, _) = SimpleAgent::ephemeral(Some("ed25519"))
2573 .map_err(|e| format!("Failed to create verifier: {}", e))?;
2574 a
2575 };
2576
2577 match agent.verify(&document) {
2578 Ok(r) => {
2579 if json_output {
2580 let out = json!({
2581 "valid": r.valid,
2582 "signerId": r.signer_id,
2583 "timestamp": r.timestamp,
2584 });
2585 println!("{}", serde_json::to_string_pretty(&out).unwrap());
2586 } else {
2587 println!("Status: {}", if r.valid { "VALID" } else { "INVALID" });
2588 println!(
2589 "Signer: {}",
2590 if r.signer_id.is_empty() {
2591 "(unknown)"
2592 } else {
2593 &r.signer_id
2594 }
2595 );
2596 if !r.timestamp.is_empty() {
2597 println!("Signed at: {}", r.timestamp);
2598 }
2599 }
2600 if !r.valid {
2601 process::exit(1);
2602 }
2603 }
2604 Err(e) => {
2605 if json_output {
2606 let out = json!({
2607 "valid": false,
2608 "error": e.to_string(),
2609 });
2610 println!("{}", serde_json::to_string_pretty(&out).unwrap());
2611 } else {
2612 eprintln!("Verification error: {}", e);
2613 }
2614 process::exit(1);
2615 }
2616 }
2617 }
2618 Some(("convert", convert_matches)) => {
2619 use jacs::convert::{html_to_jacs, jacs_to_html, jacs_to_yaml, yaml_to_jacs};
2620
2621 let target_format = convert_matches.get_one::<String>("to").unwrap();
2622 let source_format = convert_matches.get_one::<String>("from");
2623 let file_path = convert_matches.get_one::<String>("file").unwrap();
2624 let output_path = convert_matches.get_one::<String>("output");
2625
2626 let is_stdin = file_path == "-";
2628 let detected_format = if let Some(fmt) = source_format {
2629 fmt.clone()
2630 } else if is_stdin {
2631 eprintln!(
2632 "When reading from stdin (-f -), --from is required to specify the source format."
2633 );
2634 process::exit(1);
2635 } else {
2636 let ext = std::path::Path::new(file_path)
2637 .extension()
2638 .and_then(|e| e.to_str())
2639 .unwrap_or("");
2640 match ext {
2641 "json" => "json".to_string(),
2642 "yaml" | "yml" => "yaml".to_string(),
2643 "html" | "htm" => "html".to_string(),
2644 _ => {
2645 eprintln!(
2646 "Cannot auto-detect format for extension '{}'. Use --from to specify.",
2647 ext
2648 );
2649 process::exit(1);
2650 }
2651 }
2652 };
2653
2654 let input = if is_stdin {
2656 let mut buf = String::new();
2657 std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)
2658 .map_err(|e| format!("Failed to read from stdin: {}", e))?;
2659 buf
2660 } else {
2661 std::fs::read_to_string(file_path)
2662 .map_err(|e| format!("Failed to read '{}': {}", file_path, e))?
2663 };
2664
2665 let output = match (detected_format.as_str(), target_format.as_str()) {
2667 ("json", "yaml") => jacs_to_yaml(&input).map_err(|e| format!("{}", e))?,
2668 ("yaml", "json") => yaml_to_jacs(&input).map_err(|e| format!("{}", e))?,
2669 ("json", "html") => jacs_to_html(&input).map_err(|e| format!("{}", e))?,
2670 ("html", "json") => html_to_jacs(&input).map_err(|e| format!("{}", e))?,
2671 ("yaml", "html") => {
2672 let json = yaml_to_jacs(&input).map_err(|e| format!("{}", e))?;
2673 jacs_to_html(&json).map_err(|e| format!("{}", e))?
2674 }
2675 ("html", "yaml") => {
2676 let json = html_to_jacs(&input).map_err(|e| format!("{}", e))?;
2677 jacs_to_yaml(&json).map_err(|e| format!("{}", e))?
2678 }
2679 (src, dst) if src == dst => {
2680 input
2682 }
2683 (src, dst) => {
2684 eprintln!("Unsupported conversion: {} -> {}", src, dst);
2685 process::exit(1);
2686 }
2687 };
2688
2689 if let Some(out_path) = output_path {
2691 std::fs::write(out_path, &output)
2692 .map_err(|e| format!("Failed to write '{}': {}", out_path, e))?;
2693 eprintln!("Written to {}", out_path);
2694 } else {
2695 print!("{}", output);
2696 }
2697 }
2698 Some(("init", init_matches)) => {
2699 let auto_yes = *init_matches.get_one::<bool>("yes").unwrap_or(&false);
2700 println!("--- Running Config Creation ---");
2701 handle_config_create()?;
2702 println!("\n--- Running Agent Creation (with keys) ---");
2703 handle_agent_create_auto(None, true, auto_yes)?;
2704 println!("\n--- JACS Initialization Complete ---");
2705 }
2706 #[cfg(feature = "keychain")]
2707 Some(("keychain", keychain_matches)) => {
2708 use jacs::keystore::keychain;
2709
2710 match keychain_matches.subcommand() {
2711 Some(("set", sub)) => {
2712 let agent_id = sub.get_one::<String>("agent-id").unwrap();
2713 let password = if let Some(pw) = sub.get_one::<String>("password") {
2714 pw.clone()
2715 } else {
2716 eprintln!("Enter password to store in keychain:");
2717 read_password().map_err(|e| format!("Failed to read password: {}", e))?
2718 };
2719 if password.trim().is_empty() {
2720 eprintln!("Error: password cannot be empty.");
2721 process::exit(1);
2722 }
2723 if let Err(e) = jacs::crypt::aes_encrypt::check_password_strength(&password) {
2725 eprintln!("Error: {}", e);
2726 process::exit(1);
2727 }
2728 keychain::store_password(agent_id, &password)?;
2729 eprintln!("Password stored in OS keychain for agent {}.", agent_id);
2730 }
2731 Some(("get", sub)) => {
2732 let agent_id = sub.get_one::<String>("agent-id").unwrap();
2733 match keychain::get_password(agent_id)? {
2734 Some(pw) => println!("{}", pw),
2735 None => {
2736 eprintln!("No password found in OS keychain for agent {}.", agent_id);
2737 process::exit(1);
2738 }
2739 }
2740 }
2741 Some(("delete", sub)) => {
2742 let agent_id = sub.get_one::<String>("agent-id").unwrap();
2743 keychain::delete_password(agent_id)?;
2744 eprintln!("Password removed from OS keychain for agent {}.", agent_id);
2745 }
2746 Some(("status", sub)) => {
2747 let agent_id = sub.get_one::<String>("agent-id").unwrap();
2748 if keychain::is_available() {
2749 match keychain::get_password(agent_id) {
2750 Ok(Some(_)) => {
2751 eprintln!("Keychain backend: available");
2752 eprintln!("Agent: {}", agent_id);
2753 eprintln!("Password: stored");
2754 }
2755 Ok(None) => {
2756 eprintln!("Keychain backend: available");
2757 eprintln!("Agent: {}", agent_id);
2758 eprintln!("Password: not stored");
2759 }
2760 Err(e) => {
2761 eprintln!("Keychain backend: error ({})", e);
2762 }
2763 }
2764 } else {
2765 eprintln!("Keychain backend: not available (feature disabled)");
2766 }
2767 }
2768 _ => {
2769 eprintln!("Unknown keychain subcommand. Use: set, get, delete, status");
2770 process::exit(1);
2771 }
2772 }
2773 }
2774 _ => {
2775 eprintln!("Invalid command or no subcommand provided. Use --help for usage.");
2777 process::exit(1); }
2779 }
2780
2781 Ok(())
2782}