myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 828d77cd8f54c39b91047247f6880d330a8cc311
parent 25784d276008b877f9527489a366d2ee65edd74c
Author: triesap <tyson@radroots.org>
Date:   Fri, 21 Aug 2026 12:22:24 +0000

cli: freeze one-pass bootstrap contract

Diffstat:
MREADME | 10++++++++++
Asrc/cli_v1.rs | 692+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 6++++++
Atests/services_hardening_cli.rs | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 875 insertions(+), 0 deletions(-)

diff --git a/README b/README @@ -13,6 +13,16 @@ Only reviewed bounded operational leaves have defaults. Bootstrap profile, instance, repo-local root, and config-path selection are CLI concerns and are not document fields. +The hardened CLI parser admits the common command tree in one parse. It +requires explicit `--profile <service-host|interactive|repo-local>` and +`--instance <validated-instance-id>` selectors, requires an absolute +`--repo-local-root` only for `repo-local`, and accepts only an optional absolute +`--config` path. Its exact command inventory is `run`; `config +init|validate|show|schema`; `state init|status|backup|restore|verify|migrate`; +`identity init|status|rekey|replace|export-public`; `status`; and `doctor`. +This parser is a pure admission boundary in the current checkpoint; Step 118 +owns removal of the prototype parser and runtime dispatch migration. + `parse_myc_config_v1` caps original bytes before decoding, checks the schema header before closed contract admission, rejects duplicate, null, unknown, and semantically inconsistent input, and returns an immutable document plus a diff --git a/src/cli_v1.rs b/src/cli_v1.rs @@ -0,0 +1,692 @@ +//! One-pass command-line admission for the hardened Myc command contract. + +use std::error::Error; +use std::ffi::OsString; +use std::fmt; +use std::path::{Component, Path, PathBuf}; + +use clap::{Parser, Subcommand, ValueEnum}; + +/// Maximum encoded length of a Myc instance identifier. +pub const MYC_INSTANCE_ID_MAX_BYTES: usize = 128; + +/// The exact bootstrap profile selected by the operator. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycBootstrapProfileV1 { + ServiceHost, + Interactive, + RepoLocal, +} + +/// The exact governed top-level Myc command inventory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycCommandV1 { + Run, + Config(MycConfigCommandV1), + State(MycStateCommandV1), + Identity(MycIdentityCommandV1), + Status, + Doctor, +} + +/// Governed configuration commands. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycConfigCommandV1 { + Init, + Validate, + Show, + Schema, +} + +/// Governed state commands. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycStateCommandV1 { + Init, + Status, + Backup, + Restore, + Verify, + Migrate, +} + +/// Governed identity commands. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycIdentityCommandV1 { + Init, + Status, + Rekey, + Replace, + ExportPublic, +} + +/// Stable source-free classification for command-line admission failures. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycCliV1ErrorKind { + InvalidArguments, + InvalidInstance, + InvalidRepoLocalRoot, + UnexpectedRepoLocalRoot, + InvalidConfigPath, +} + +impl MycCliV1ErrorKind { + const fn message(self) -> &'static str { + match self { + Self::InvalidArguments => "command-line arguments are invalid", + Self::InvalidInstance => "instance identifier is invalid", + Self::InvalidRepoLocalRoot => "repo-local profile requires a valid absolute root", + Self::UnexpectedRepoLocalRoot => { + "repo-local root is forbidden outside the repo-local profile" + } + Self::InvalidConfigPath => "configuration path must be absolute without traversal", + } + } +} + +/// One safe command-line admission failure. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MycCliV1Error { + kind: MycCliV1ErrorKind, +} + +impl MycCliV1Error { + const fn new(kind: MycCliV1ErrorKind) -> Self { + Self { kind } + } + + /// Returns the stable failure classification. + #[must_use] + pub const fn kind(self) -> MycCliV1ErrorKind { + self.kind + } +} + +impl fmt::Debug for MycCliV1Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycCliV1Error") + .field("kind", &self.kind) + .finish() + } +} + +impl fmt::Display for MycCliV1Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.kind.message()) + } +} + +impl Error for MycCliV1Error {} + +/// A validated one-pass Myc bootstrap and command selection. +pub struct MycCliInvocationV1 { + profile: MycBootstrapProfileV1, + instance: Box<str>, + repo_local_root: Option<PathBuf>, + config_path: Option<PathBuf>, + command: MycCommandV1, +} + +impl MycCliInvocationV1 { + /// Returns the explicitly selected bootstrap profile. + #[must_use] + pub const fn profile(&self) -> MycBootstrapProfileV1 { + self.profile + } + + /// Returns the validated instance identifier. + #[must_use] + pub fn instance(&self) -> &str { + &self.instance + } + + /// Returns the explicit repo-local root, when selected. + #[must_use] + pub fn repo_local_root(&self) -> Option<&Path> { + self.repo_local_root.as_deref() + } + + /// Returns the optional explicit configuration path. + #[must_use] + pub fn config_path(&self) -> Option<&Path> { + self.config_path.as_deref() + } + + /// Returns the exact governed command selection. + #[must_use] + pub const fn command(&self) -> MycCommandV1 { + self.command + } +} + +impl fmt::Debug for MycCliInvocationV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycCliInvocationV1") + .field("profile", &self.profile) + .field("instance", &"[redacted]") + .field( + "repo_local_root", + &self.repo_local_root.as_ref().map(|_| "[redacted]"), + ) + .field( + "config_path", + &self.config_path.as_ref().map(|_| "[redacted]"), + ) + .field("command", &self.command) + .finish() + } +} + +/// Parses the exact hardened Myc bootstrap and command tree once. +/// +/// The iterator must include the program name as its first element. Clap's +/// dependency-owned diagnostic is deliberately discarded so caller-controlled +/// argument text cannot escape through this crate's stable error boundary. +pub fn parse_myc_cli_v1_from<I, T>(arguments: I) -> Result<MycCliInvocationV1, MycCliV1Error> +where + I: IntoIterator<Item = T>, + T: Into<OsString> + Clone, +{ + let parsed = RawMycCliV1::try_parse_from(arguments) + .map_err(|_| MycCliV1Error::new(MycCliV1ErrorKind::InvalidArguments))?; + let profile = parsed + .profile + .ok_or_else(|| MycCliV1Error::new(MycCliV1ErrorKind::InvalidArguments))? + .into(); + let instance = parsed + .instance + .ok_or_else(|| MycCliV1Error::new(MycCliV1ErrorKind::InvalidArguments))?; + validate_instance(&instance)?; + validate_bootstrap_paths( + profile, + parsed.repo_local_root.as_deref(), + parsed.config.as_deref(), + )?; + + Ok(MycCliInvocationV1 { + profile, + instance: instance.into_boxed_str(), + repo_local_root: parsed.repo_local_root, + config_path: parsed.config, + command: parsed.command.into(), + }) +} + +fn validate_instance(value: &str) -> Result<(), MycCliV1Error> { + let bytes = value.as_bytes(); + let is_boundary = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit(); + if bytes.is_empty() + || bytes.len() > MYC_INSTANCE_ID_MAX_BYTES + || !is_boundary(bytes[0]) + || !is_boundary(bytes[bytes.len() - 1]) + || !bytes + .iter() + .all(|byte| is_boundary(*byte) || matches!(*byte, b'-' | b'_')) + { + return Err(MycCliV1Error::new(MycCliV1ErrorKind::InvalidInstance)); + } + Ok(()) +} + +fn validate_bootstrap_paths( + profile: MycBootstrapProfileV1, + repo_local_root: Option<&Path>, + config_path: Option<&Path>, +) -> Result<(), MycCliV1Error> { + match (profile, repo_local_root) { + (MycBootstrapProfileV1::RepoLocal, Some(root)) if valid_absolute_path(root, true) => {} + (MycBootstrapProfileV1::RepoLocal, _) => { + return Err(MycCliV1Error::new(MycCliV1ErrorKind::InvalidRepoLocalRoot)); + } + (_, Some(_)) => { + return Err(MycCliV1Error::new( + MycCliV1ErrorKind::UnexpectedRepoLocalRoot, + )); + } + (_, None) => {} + } + + if config_path.is_some_and(|path| !valid_absolute_path(path, true)) { + return Err(MycCliV1Error::new(MycCliV1ErrorKind::InvalidConfigPath)); + } + Ok(()) +} + +fn valid_absolute_path(path: &Path, require_non_root: bool) -> bool { + path.is_absolute() + && (!require_non_root || path.parent().is_some()) + && !path + .components() + .any(|component| matches!(component, Component::ParentDir)) +} + +#[derive(Parser)] +#[command(name = "myc", disable_help_subcommand = true)] +struct RawMycCliV1 { + #[arg(long, global = true, value_enum)] + profile: Option<RawProfile>, + #[arg(long, global = true)] + instance: Option<String>, + #[arg(long = "repo-local-root", global = true)] + repo_local_root: Option<PathBuf>, + #[arg(long, global = true)] + config: Option<PathBuf>, + #[command(subcommand)] + command: RawCommand, +} + +#[derive(Clone, Copy, ValueEnum)] +enum RawProfile { + ServiceHost, + Interactive, + RepoLocal, +} + +impl From<RawProfile> for MycBootstrapProfileV1 { + fn from(value: RawProfile) -> Self { + match value { + RawProfile::ServiceHost => Self::ServiceHost, + RawProfile::Interactive => Self::Interactive, + RawProfile::RepoLocal => Self::RepoLocal, + } + } +} + +#[derive(Subcommand)] +enum RawCommand { + Run, + Config { + #[command(subcommand)] + command: RawConfigCommand, + }, + State { + #[command(subcommand)] + command: RawStateCommand, + }, + Identity { + #[command(subcommand)] + command: RawIdentityCommand, + }, + Status, + Doctor, +} + +impl From<RawCommand> for MycCommandV1 { + fn from(value: RawCommand) -> Self { + match value { + RawCommand::Run => Self::Run, + RawCommand::Config { command } => Self::Config(command.into()), + RawCommand::State { command } => Self::State(command.into()), + RawCommand::Identity { command } => Self::Identity(command.into()), + RawCommand::Status => Self::Status, + RawCommand::Doctor => Self::Doctor, + } + } +} + +#[derive(Subcommand)] +enum RawConfigCommand { + Init, + Validate, + Show, + Schema, +} + +impl From<RawConfigCommand> for MycConfigCommandV1 { + fn from(value: RawConfigCommand) -> Self { + match value { + RawConfigCommand::Init => Self::Init, + RawConfigCommand::Validate => Self::Validate, + RawConfigCommand::Show => Self::Show, + RawConfigCommand::Schema => Self::Schema, + } + } +} + +#[derive(Subcommand)] +enum RawStateCommand { + Init, + Status, + Backup, + Restore, + Verify, + Migrate, +} + +impl From<RawStateCommand> for MycStateCommandV1 { + fn from(value: RawStateCommand) -> Self { + match value { + RawStateCommand::Init => Self::Init, + RawStateCommand::Status => Self::Status, + RawStateCommand::Backup => Self::Backup, + RawStateCommand::Restore => Self::Restore, + RawStateCommand::Verify => Self::Verify, + RawStateCommand::Migrate => Self::Migrate, + } + } +} + +#[derive(Subcommand)] +enum RawIdentityCommand { + Init, + Status, + Rekey, + Replace, + ExportPublic, +} + +impl From<RawIdentityCommand> for MycIdentityCommandV1 { + fn from(value: RawIdentityCommand) -> Self { + match value { + RawIdentityCommand::Init => Self::Init, + RawIdentityCommand::Status => Self::Status, + RawIdentityCommand::Rekey => Self::Rekey, + RawIdentityCommand::Replace => Self::Replace, + RawIdentityCommand::ExportPublic => Self::ExportPublic, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(command: &[&str]) -> Result<MycCliInvocationV1, MycCliV1Error> { + let mut arguments = vec!["myc", "--profile", "service-host", "--instance", "primary"]; + arguments.extend_from_slice(command); + parse_myc_cli_v1_from(arguments) + } + + #[test] + fn exact_command_inventory_parses() { + let vectors = [ + (&["run"][..], MycCommandV1::Run), + ( + &["config", "init"][..], + MycCommandV1::Config(MycConfigCommandV1::Init), + ), + ( + &["config", "validate"][..], + MycCommandV1::Config(MycConfigCommandV1::Validate), + ), + ( + &["config", "show"][..], + MycCommandV1::Config(MycConfigCommandV1::Show), + ), + ( + &["config", "schema"][..], + MycCommandV1::Config(MycConfigCommandV1::Schema), + ), + ( + &["state", "init"][..], + MycCommandV1::State(MycStateCommandV1::Init), + ), + ( + &["state", "status"][..], + MycCommandV1::State(MycStateCommandV1::Status), + ), + ( + &["state", "backup"][..], + MycCommandV1::State(MycStateCommandV1::Backup), + ), + ( + &["state", "restore"][..], + MycCommandV1::State(MycStateCommandV1::Restore), + ), + ( + &["state", "verify"][..], + MycCommandV1::State(MycStateCommandV1::Verify), + ), + ( + &["state", "migrate"][..], + MycCommandV1::State(MycStateCommandV1::Migrate), + ), + ( + &["identity", "init"][..], + MycCommandV1::Identity(MycIdentityCommandV1::Init), + ), + ( + &["identity", "status"][..], + MycCommandV1::Identity(MycIdentityCommandV1::Status), + ), + ( + &["identity", "rekey"][..], + MycCommandV1::Identity(MycIdentityCommandV1::Rekey), + ), + ( + &["identity", "replace"][..], + MycCommandV1::Identity(MycIdentityCommandV1::Replace), + ), + ( + &["identity", "export-public"][..], + MycCommandV1::Identity(MycIdentityCommandV1::ExportPublic), + ), + (&["status"][..], MycCommandV1::Status), + (&["doctor"][..], MycCommandV1::Doctor), + ]; + for (arguments, expected) in vectors { + assert_eq!(parse(arguments).expect("command").command(), expected); + } + } + + #[test] + fn profiles_and_paths_are_cross_bound() { + for profile in ["service-host", "interactive"] { + let invocation = parse_myc_cli_v1_from([ + "myc", + "--profile", + profile, + "--instance", + "north-01", + "--config", + "/etc/radroots/myc.toml", + "run", + ]) + .expect("production profile"); + assert_eq!(invocation.instance(), "north-01"); + assert_eq!( + invocation.config_path(), + Some(Path::new("/etc/radroots/myc.toml")) + ); + assert!(invocation.repo_local_root().is_none()); + } + + let repo_local = parse_myc_cli_v1_from([ + "myc", + "--profile", + "repo-local", + "--instance", + "dev", + "--repo-local-root", + "/repo/radroots", + "config", + "validate", + ]) + .expect("repo local"); + assert_eq!(repo_local.profile(), MycBootstrapProfileV1::RepoLocal); + assert_eq!( + repo_local.repo_local_root(), + Some(Path::new("/repo/radroots")) + ); + } + + #[test] + fn invalid_bootstrap_values_fail_with_stable_kinds() { + for value in ["Upper", "north-", "north.west"] { + let error = parse_myc_cli_v1_from([ + "myc", + "--profile", + "service-host", + "--instance", + value, + "run", + ]) + .expect_err("invalid instance"); + assert_eq!(error.kind(), MycCliV1ErrorKind::InvalidInstance); + } + assert_eq!( + parse_myc_cli_v1_from([ + "myc", + "--profile", + "service-host", + "--instance=-north", + "run", + ]) + .expect_err("invalid instance boundary") + .kind(), + MycCliV1ErrorKind::InvalidInstance + ); + assert_eq!( + parse_myc_cli_v1_from(["myc", "--profile", "service-host", "--instance=", "run",]) + .expect_err("empty instance") + .kind(), + MycCliV1ErrorKind::InvalidInstance + ); + let exact = "a".repeat(MYC_INSTANCE_ID_MAX_BYTES); + assert!( + parse_myc_cli_v1_from([ + "myc", + "--profile", + "service-host", + "--instance", + exact.as_str(), + "run", + ]) + .is_ok() + ); + let overlong = "a".repeat(MYC_INSTANCE_ID_MAX_BYTES + 1); + assert_eq!( + parse_myc_cli_v1_from([ + "myc", + "--profile", + "service-host", + "--instance", + overlong.as_str(), + "run", + ]) + .expect_err("overlong instance") + .kind(), + MycCliV1ErrorKind::InvalidInstance + ); + + let missing_root = + parse_myc_cli_v1_from(["myc", "--profile", "repo-local", "--instance", "dev", "run"]) + .expect_err("missing root"); + assert_eq!(missing_root.kind(), MycCliV1ErrorKind::InvalidRepoLocalRoot); + + let unexpected_root = parse_myc_cli_v1_from([ + "myc", + "--profile", + "interactive", + "--instance", + "dev", + "--repo-local-root", + "/repo/radroots", + "run", + ]) + .expect_err("unexpected root"); + assert_eq!( + unexpected_root.kind(), + MycCliV1ErrorKind::UnexpectedRepoLocalRoot + ); + + for invalid in ["relative", "/", "/repo/../escape"] { + let error = parse_myc_cli_v1_from([ + "myc", + "--profile", + "repo-local", + "--instance", + "dev", + "--repo-local-root", + invalid, + "run", + ]) + .expect_err("invalid root"); + assert_eq!(error.kind(), MycCliV1ErrorKind::InvalidRepoLocalRoot); + } + + for invalid in ["relative.toml", "/", "/etc/../secret.toml"] { + let error = parse_myc_cli_v1_from([ + "myc", + "--profile", + "service-host", + "--instance", + "primary", + "--config", + invalid, + "run", + ]) + .expect_err("invalid config path"); + assert_eq!(error.kind(), MycCliV1ErrorKind::InvalidConfigPath); + } + } + + #[test] + fn missing_unknown_and_prototype_arguments_fail_without_sources() { + for arguments in [ + vec!["myc", "run"], + vec!["myc", "--profile", "service-host", "run"], + vec!["myc", "--profile", "service-host", "--instance", "primary"], + vec![ + "myc", + "--profile", + "production", + "--instance", + "primary", + "run", + ], + vec![ + "myc", + "--profile", + "service-host", + "--instance", + "primary", + "--env-file", + "secret.env", + "run", + ], + vec![ + "myc", + "--profile", + "service-host", + "--instance", + "primary", + "persistence", + "backup", + ], + ] { + let failure = parse_myc_cli_v1_from(arguments).expect_err("arguments must fail"); + assert_eq!(failure.kind(), MycCliV1ErrorKind::InvalidArguments); + assert!(Error::source(&failure).is_none()); + } + } + + #[test] + fn debug_and_errors_do_not_render_caller_values() { + let instance = "sensitive-instance"; + let config = "/sensitive/config.toml"; + let invocation = parse_myc_cli_v1_from([ + "myc", + "--profile", + "service-host", + "--instance", + instance, + "--config", + config, + "doctor", + ]) + .expect("invocation"); + let debug = format!("{invocation:?}"); + assert!(!debug.contains(instance)); + assert!(!debug.contains(config)); + + let secret = "secret-cli-value"; + let failure = + parse_myc_cli_v1_from(["myc", "--profile", secret, "--instance", "primary", "run"]) + .expect_err("invalid profile"); + let rendered = format!("{failure} {failure:?}"); + assert!(!rendered.contains(secret)); + assert!(Error::source(&failure).is_none()); + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -5,6 +5,7 @@ pub mod app; pub mod audit; mod audit_sqlite; pub mod cli; +mod cli_v1; pub mod config; mod config_v1; pub mod control; @@ -34,6 +35,11 @@ pub use audit::{ MycOperationAuditRecord, MycOperationAuditStore, }; pub use audit_sqlite::MycSqliteOperationAuditStore; +pub use cli_v1::{ + MYC_INSTANCE_ID_MAX_BYTES, MycBootstrapProfileV1, MycCliInvocationV1, MycCliV1Error, + MycCliV1ErrorKind, MycCommandV1, MycConfigCommandV1, MycIdentityCommandV1, MycStateCommandV1, + parse_myc_cli_v1_from, +}; pub use config::{ DEFAULT_ENV_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycCustodyConfig, MycDiscoveryConfig, MycDiscoveryMetadataConfig, MycIdentityBackend, MycIdentitySourceSpec, diff --git a/tests/services_hardening_cli.rs b/tests/services_hardening_cli.rs @@ -0,0 +1,167 @@ +#![forbid(unsafe_code)] + +use std::error::Error; +use std::path::Path; + +use myc::{ + MYC_INSTANCE_ID_MAX_BYTES, MycBootstrapProfileV1, MycCliV1ErrorKind, MycCommandV1, + MycConfigCommandV1, MycIdentityCommandV1, MycStateCommandV1, parse_myc_cli_v1_from, +}; + +const CLI_SOURCE: &str = include_str!("../src/cli_v1.rs"); + +fn base(command: &[&str]) -> Vec<String> { + let mut arguments = vec![ + "myc".to_owned(), + "--profile".to_owned(), + "service-host".to_owned(), + "--instance".to_owned(), + "primary".to_owned(), + ]; + arguments.extend(command.iter().map(|value| (*value).to_owned())); + arguments +} + +#[test] +fn root_api_freezes_the_exact_command_inventory() { + let vectors = [ + (vec!["run"], MycCommandV1::Run), + ( + vec!["config", "init"], + MycCommandV1::Config(MycConfigCommandV1::Init), + ), + ( + vec!["config", "validate"], + MycCommandV1::Config(MycConfigCommandV1::Validate), + ), + ( + vec!["config", "show"], + MycCommandV1::Config(MycConfigCommandV1::Show), + ), + ( + vec!["config", "schema"], + MycCommandV1::Config(MycConfigCommandV1::Schema), + ), + ( + vec!["state", "init"], + MycCommandV1::State(MycStateCommandV1::Init), + ), + ( + vec!["state", "status"], + MycCommandV1::State(MycStateCommandV1::Status), + ), + ( + vec!["state", "backup"], + MycCommandV1::State(MycStateCommandV1::Backup), + ), + ( + vec!["state", "restore"], + MycCommandV1::State(MycStateCommandV1::Restore), + ), + ( + vec!["state", "verify"], + MycCommandV1::State(MycStateCommandV1::Verify), + ), + ( + vec!["state", "migrate"], + MycCommandV1::State(MycStateCommandV1::Migrate), + ), + ( + vec!["identity", "init"], + MycCommandV1::Identity(MycIdentityCommandV1::Init), + ), + ( + vec!["identity", "status"], + MycCommandV1::Identity(MycIdentityCommandV1::Status), + ), + ( + vec!["identity", "rekey"], + MycCommandV1::Identity(MycIdentityCommandV1::Rekey), + ), + ( + vec!["identity", "replace"], + MycCommandV1::Identity(MycIdentityCommandV1::Replace), + ), + ( + vec!["identity", "export-public"], + MycCommandV1::Identity(MycIdentityCommandV1::ExportPublic), + ), + (vec!["status"], MycCommandV1::Status), + (vec!["doctor"], MycCommandV1::Doctor), + ]; + + for (arguments, expected) in vectors { + assert_eq!( + parse_myc_cli_v1_from(base(&arguments)) + .expect("governed command") + .command(), + expected + ); + } +} + +#[test] +fn root_api_exposes_validated_cross_bound_bootstrap_values() { + let invocation = parse_myc_cli_v1_from([ + "myc", + "config", + "validate", + "--profile", + "repo-local", + "--instance", + "dev-01", + "--repo-local-root", + "/repo/radroots", + "--config", + "/repo/radroots/config/services/myc/config.toml", + ]) + .expect("repo-local invocation"); + assert_eq!(invocation.profile(), MycBootstrapProfileV1::RepoLocal); + assert_eq!(invocation.instance(), "dev-01"); + assert_eq!( + invocation.repo_local_root(), + Some(Path::new("/repo/radroots")) + ); + assert_eq!( + invocation.config_path(), + Some(Path::new("/repo/radroots/config/services/myc/config.toml")) + ); + assert_eq!(MYC_INSTANCE_ID_MAX_BYTES, 128); +} + +#[test] +fn root_api_rejects_prototype_and_arbitrary_leaf_arguments_safely() { + for arguments in [ + base(&["--env-file", "/secret/config.env", "run"]), + base(&["metrics"]), + base(&["persistence", "backup"]), + base(&["identity", "generate"]), + base(&["run", "--relay-url", "wss://secret.example"]), + ] { + let error = parse_myc_cli_v1_from(arguments).expect_err("forbidden CLI shape"); + assert_eq!(error.kind(), MycCliV1ErrorKind::InvalidArguments); + assert!(Error::source(&error).is_none()); + let rendered = format!("{error} {error:?}"); + assert!(!rendered.contains("secret")); + } +} + +#[test] +fn parser_is_single_pass_pure_and_privately_implemented() { + assert_eq!(CLI_SOURCE.matches("RawMycCliV1::try_parse_from").count(), 1); + for forbidden in [ + "std::env::", + "env::args", + "std::fs::", + "tokio::", + "serde_json::", + "toml::", + "pub mod cli_v1", + ] { + assert!(!CLI_SOURCE.contains(forbidden), "found `{forbidden}`"); + } + + let root = include_str!("../src/lib.rs"); + assert!(root.contains("mod cli_v1;")); + assert!(!root.contains("pub mod cli_v1;")); +}