Skip to main content

jacs_cli/
main.rs

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