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