myc

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

commit 343a9f76c1298f9a8d1e2b37cc1f3c1bff54d3b2
parent 830a5a4f05ba6a1233f5144a80bd5de1516c4322
Author: triesap <tyson@radroots.org>
Date:   Sat, 22 Aug 2026 05:13:38 +0000

cli: freeze one-pass execution authority

Diffstat:
MAGENTS.md | 7+++++++
MREADME | 8++++++++
Mcontracts/api_baselines/myc.txt | 30++++++++++++++++++++++++++++++
Mcontracts/services_hardening/operator_contract.v1.json | 27+++++++++++++++++++++++++++
Msrc/cli_v1.rs | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 6++++--
Msrc/main.rs | 3++-
Mtests/package_boundary.rs | 5+++++
Mtests/services_hardening_cli.rs | 357++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
9 files changed, 640 insertions(+), 5 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -107,6 +107,13 @@ cursors to route/filter/snapshot identity and must retain exact operation-ID replay/conflict evidence before returning success. Do not infer relay delivery from a successful local mutation and do not add TCP admin routes. +- Step 152 freezes the one-parse CLI execution split. Every admitted command + must select exactly daemon, offline, or permissioned Unix-admin authority; + only the explicitly contracted read-only status, backup, and public-identity + operations may fall back after proving the daemon writer lock is free. + Rekey, replace, and every other live mutation have no direct-state fallback. + Do not reparse process arguments or let a live CLI plan obtain SQLite, + provider, relay, task, signal, or runtime authority. - Treat checked-in source, tests, and prototype behavior as implementation evidence, not permission to preserve behavior that the active requirement removes. diff --git a/README b/README @@ -52,6 +52,14 @@ The process binary uses only this parser. Hardened command execution remains fail-closed until its ordered runtime-dispatch steps are complete; no prototype command is used as a fallback. +The parsed invocation is projected once into a sealed execution plan. `run` +selects daemon authority; config, exclusive state/identity provisioning, and +doctor select offline authority; state/identity/service inspection and live +mutations select the permissioned Unix-admin authority. Only explicitly +read-only status, backup, and public-identity operations may fall back when a +later executor proves the daemon writer lock is free. Rekey and replace never +fall back to direct state access, and no live plan carries SQLite authority. + `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/contracts/api_baselines/myc.txt b/contracts/api_baselines/myc.txt @@ -103,6 +103,27 @@ pub enum myc::MycBootstrapProfileV1 pub myc::MycBootstrapProfileV1::Interactive pub myc::MycBootstrapProfileV1::RepoLocal pub myc::MycBootstrapProfileV1::ServiceHost +pub enum myc::MycCliAdminOperationV1 +pub myc::MycCliAdminOperationV1::IdentityPublic +pub myc::MycCliAdminOperationV1::IdentityRekey +pub myc::MycCliAdminOperationV1::IdentityReplace +pub myc::MycCliAdminOperationV1::IdentityStatus +pub myc::MycCliAdminOperationV1::StateBackup +pub myc::MycCliAdminOperationV1::StateStatus +pub myc::MycCliAdminOperationV1::Status +impl myc::MycCliAdminOperationV1 +pub const fn myc::MycCliAdminOperationV1::route(self) -> myc::MycAdminRoute +pub enum myc::MycCliOfflineOperationV1 +pub myc::MycCliOfflineOperationV1::Config +pub myc::MycCliOfflineOperationV1::Doctor +pub myc::MycCliOfflineOperationV1::IdentityExclusive +pub myc::MycCliOfflineOperationV1::IdentityReadOnly +pub myc::MycCliOfflineOperationV1::StateExclusive +pub myc::MycCliOfflineOperationV1::StateReadOnly +pub enum myc::MycCliPrimaryAuthorityV1 +pub myc::MycCliPrimaryAuthorityV1::Daemon +pub myc::MycCliPrimaryAuthorityV1::LiveUnixAdmin +pub myc::MycCliPrimaryAuthorityV1::Offline pub enum myc::MycCliV1ErrorKind pub myc::MycCliV1ErrorKind::InvalidArguments pub myc::MycCliV1ErrorKind::InvalidConfigPath @@ -712,6 +733,14 @@ pub fn myc::MycBoundedNip46Request::plaintext_bytes(&self) -> usize pub const fn myc::MycBoundedNip46Request::request_id_bytes(&self) -> usize impl core::fmt::Debug for myc::MycBoundedNip46Request pub fn myc::MycBoundedNip46Request::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycCliExecutionPlanV1 +impl myc::MycCliExecutionPlanV1 +pub const fn myc::MycCliExecutionPlanV1::admin_operation(&self) -> core::option::Option<myc::MycCliAdminOperationV1> +pub const fn myc::MycCliExecutionPlanV1::allows_daemon_unavailable_offline_fallback(&self) -> bool +pub const fn myc::MycCliExecutionPlanV1::offline_operation(&self) -> core::option::Option<myc::MycCliOfflineOperationV1> +pub const fn myc::MycCliExecutionPlanV1::primary_authority(&self) -> myc::MycCliPrimaryAuthorityV1 +impl core::fmt::Debug for myc::MycCliExecutionPlanV1 +pub fn myc::MycCliExecutionPlanV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycCliInvocationV1 impl myc::MycCliInvocationV1 pub const fn myc::MycCliInvocationV1::command(&self) -> myc::MycCommandV1 @@ -1700,6 +1729,7 @@ pub async fn myc::open_myc_state_inspection(&myc::MycRuntimeContext, &myc::MycSt pub async fn myc::open_myc_state_read_write(&myc::MycRuntimeContext, &myc::MycStateMetadata, radroots_service_sqlite::migration::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<myc::MycStateHost, myc::MycStateHostError> pub fn myc::parse_myc_cli_v1_from<I, T>(I) -> core::result::Result<myc::MycCliInvocationV1, myc::MycCliV1Error> where I: core::iter::traits::collect::IntoIterator<Item = T>, T: core::convert::Into<std::ffi::os_str::OsString> + core::clone::Clone pub fn myc::parse_myc_config_v1(&[u8], myc::MycConfigProfile) -> core::result::Result<myc::MycConfigDocumentV1, myc::MycConfigV1Error> +pub const fn myc::plan_myc_cli_v1(&myc::MycCliInvocationV1) -> myc::MycCliExecutionPlanV1 pub fn myc::prepare_myc_nip46_decrypt_work(myc::MycVerifiedNip46Event, &myc::MycProviderBinding, myc::MycProviderDeadlineUnixMs) -> core::result::Result<myc::MycNip46DecryptWork, myc::MycNip46WorkError> pub fn myc::prepare_myc_nip46_request(myc::MycDecryptedNip46Request, myc::MycSignerOperationNonce, myc::MycRequestReceivedAtUnixMs) -> core::result::Result<myc::MycPreparedNip46Request, myc::MycNip46WorkError> pub fn myc::prepare_myc_nip46_work(myc::MycPreparedNip46Request, myc::MycSignerRequestRecord, core::option::Option<myc::MycConnectionRecord>, &myc::MycProviderContract, myc::MycConnectionTimeUnixMs, core::option::Option<myc::MycProviderDeadlineUnixMs>) -> core::result::Result<myc::MycNip46Work, myc::MycNip46WorkError> diff --git a/contracts/services_hardening/operator_contract.v1.json b/contracts/services_hardening/operator_contract.v1.json @@ -242,6 +242,33 @@ } } }, + "cli_dispatch": { + "parse_count": 1, + "primary_authorities": ["daemon", "offline", "live_unix_admin"], + "live_direct_sqlite_access": false, + "live_mutation_offline_fallback": false, + "read_only_offline_fallback_requires_free_daemon_writer_lock": true, + "commands": [ + { "command": "run", "primary_authority": "daemon" }, + { "command": "config init", "primary_authority": "offline", "offline_operation": "config" }, + { "command": "config validate", "primary_authority": "offline", "offline_operation": "config" }, + { "command": "config show", "primary_authority": "offline", "offline_operation": "config" }, + { "command": "config schema", "primary_authority": "offline", "offline_operation": "config" }, + { "command": "state init", "primary_authority": "offline", "offline_operation": "state_exclusive" }, + { "command": "state status", "primary_authority": "live_unix_admin", "admin_route": "/v1/state/status", "offline_operation": "state_read_only", "daemon_unavailable_offline_fallback": true }, + { "command": "state backup", "primary_authority": "live_unix_admin", "admin_route": "/v1/state/backup", "offline_operation": "state_read_only", "daemon_unavailable_offline_fallback": true }, + { "command": "state restore", "primary_authority": "offline", "offline_operation": "state_exclusive" }, + { "command": "state verify", "primary_authority": "offline", "offline_operation": "state_exclusive" }, + { "command": "state migrate", "primary_authority": "offline", "offline_operation": "state_exclusive" }, + { "command": "identity init", "primary_authority": "offline", "offline_operation": "identity_exclusive" }, + { "command": "identity status", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/status", "offline_operation": "identity_read_only", "daemon_unavailable_offline_fallback": true }, + { "command": "identity rekey", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/rekey" }, + { "command": "identity replace", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/replace" }, + { "command": "identity export-public", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/public", "offline_operation": "identity_read_only", "daemon_unavailable_offline_fallback": true }, + { "command": "status", "primary_authority": "live_unix_admin", "admin_route": "/v1/status", "offline_operation": "state_read_only", "daemon_unavailable_offline_fallback": true }, + { "command": "doctor", "primary_authority": "offline", "offline_operation": "doctor" } + ] + }, "doctor": { "shared_schema": "radroots.service.doctor.v1", "contract_version": 1, diff --git a/src/cli_v1.rs b/src/cli_v1.rs @@ -58,6 +58,120 @@ pub enum MycIdentityCommandV1 { ExportPublic, } +/// The only three process authorities selected by the hardened CLI. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycCliPrimaryAuthorityV1 { + Daemon, + Offline, + LiveUnixAdmin, +} + +/// The closed offline operation classes selected before any state access. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycCliOfflineOperationV1 { + Config, + StateExclusive, + StateReadOnly, + IdentityExclusive, + IdentityReadOnly, + Doctor, +} + +/// The closed Unix-admin operations reachable from the command inventory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycCliAdminOperationV1 { + Status, + StateStatus, + StateBackup, + IdentityStatus, + IdentityRekey, + IdentityReplace, + IdentityPublic, +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +impl MycCliAdminOperationV1 { + /// Returns the exact native Unix-admin route selected by this operation. + #[must_use] + pub const fn route(self) -> crate::MycAdminRoute { + match self { + Self::Status => crate::MycAdminRoute::Status, + Self::StateStatus => crate::MycAdminRoute::StateStatus, + Self::StateBackup => crate::MycAdminRoute::StateBackup, + Self::IdentityStatus => crate::MycAdminRoute::IdentityStatus, + Self::IdentityRekey => crate::MycAdminRoute::IdentityRekey, + Self::IdentityReplace => crate::MycAdminRoute::IdentityReplace, + Self::IdentityPublic => crate::MycAdminRoute::IdentityPublic, + } + } +} + +/// A sealed, side-effect-free execution plan for one admitted CLI invocation. +/// +/// Construction is owned by [`plan_myc_cli_v1`]. A live mutation never carries +/// an offline fallback, while explicitly read-only status, backup, and public +/// identity operations may fall back only after later execution proves the +/// daemon writer lock is free. +/// +/// ```compile_fail +/// use myc::{MycCliExecutionPlanV1, MycCliPrimaryAuthorityV1}; +/// +/// let _ = MycCliExecutionPlanV1 { +/// primary_authority: MycCliPrimaryAuthorityV1::Offline, +/// offline_operation: None, +/// admin_operation: None, +/// daemon_unavailable_offline_fallback: true, +/// }; +/// ``` +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MycCliExecutionPlanV1 { + primary_authority: MycCliPrimaryAuthorityV1, + offline_operation: Option<MycCliOfflineOperationV1>, + admin_operation: Option<MycCliAdminOperationV1>, + daemon_unavailable_offline_fallback: bool, +} + +impl MycCliExecutionPlanV1 { + /// Returns the authority that must be attempted first. + #[must_use] + pub const fn primary_authority(&self) -> MycCliPrimaryAuthorityV1 { + self.primary_authority + } + + /// Returns the bounded offline operation, when the plan admits one. + #[must_use] + pub const fn offline_operation(&self) -> Option<MycCliOfflineOperationV1> { + self.offline_operation + } + + /// Returns the bounded Unix-admin operation, when the plan admits one. + #[must_use] + pub const fn admin_operation(&self) -> Option<MycCliAdminOperationV1> { + self.admin_operation + } + + /// Returns whether a missing daemon may fall back to read-only offline work. + #[must_use] + pub const fn allows_daemon_unavailable_offline_fallback(&self) -> bool { + self.daemon_unavailable_offline_fallback + } +} + +impl fmt::Debug for MycCliExecutionPlanV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycCliExecutionPlanV1") + .field("primary_authority", &self.primary_authority) + .field("offline_operation", &self.offline_operation) + .field("admin_operation", &self.admin_operation) + .field( + "daemon_unavailable_offline_fallback", + &self.daemon_unavailable_offline_fallback, + ) + .finish() + } +} + /// Stable source-free classification for command-line admission failures. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MycCliV1ErrorKind { @@ -213,6 +327,94 @@ where }) } +/// Selects the sole permitted execution authority for an admitted command. +/// +/// This function performs no filesystem, database, socket, environment, task, +/// or process work. Later executors consume the plan without reparsing process +/// arguments. In particular, no live command receives direct SQLite authority. +#[must_use] +pub const fn plan_myc_cli_v1(invocation: &MycCliInvocationV1) -> MycCliExecutionPlanV1 { + match invocation.command { + MycCommandV1::Run => daemon_plan(), + MycCommandV1::Config(_) => offline_plan(MycCliOfflineOperationV1::Config), + MycCommandV1::State(MycStateCommandV1::Init) + | MycCommandV1::State(MycStateCommandV1::Restore) + | MycCommandV1::State(MycStateCommandV1::Verify) + | MycCommandV1::State(MycStateCommandV1::Migrate) => { + offline_plan(MycCliOfflineOperationV1::StateExclusive) + } + MycCommandV1::State(MycStateCommandV1::Status) => read_only_admin_plan( + MycCliAdminOperationV1::StateStatus, + MycCliOfflineOperationV1::StateReadOnly, + ), + MycCommandV1::State(MycStateCommandV1::Backup) => read_only_admin_plan( + MycCliAdminOperationV1::StateBackup, + MycCliOfflineOperationV1::StateReadOnly, + ), + MycCommandV1::Identity(MycIdentityCommandV1::Init) => { + offline_plan(MycCliOfflineOperationV1::IdentityExclusive) + } + MycCommandV1::Identity(MycIdentityCommandV1::Status) => read_only_admin_plan( + MycCliAdminOperationV1::IdentityStatus, + MycCliOfflineOperationV1::IdentityReadOnly, + ), + MycCommandV1::Identity(MycIdentityCommandV1::ExportPublic) => read_only_admin_plan( + MycCliAdminOperationV1::IdentityPublic, + MycCliOfflineOperationV1::IdentityReadOnly, + ), + MycCommandV1::Identity(MycIdentityCommandV1::Rekey) => { + admin_plan(MycCliAdminOperationV1::IdentityRekey) + } + MycCommandV1::Identity(MycIdentityCommandV1::Replace) => { + admin_plan(MycCliAdminOperationV1::IdentityReplace) + } + MycCommandV1::Status => read_only_admin_plan( + MycCliAdminOperationV1::Status, + MycCliOfflineOperationV1::StateReadOnly, + ), + MycCommandV1::Doctor => offline_plan(MycCliOfflineOperationV1::Doctor), + } +} + +const fn daemon_plan() -> MycCliExecutionPlanV1 { + MycCliExecutionPlanV1 { + primary_authority: MycCliPrimaryAuthorityV1::Daemon, + offline_operation: None, + admin_operation: None, + daemon_unavailable_offline_fallback: false, + } +} + +const fn offline_plan(operation: MycCliOfflineOperationV1) -> MycCliExecutionPlanV1 { + MycCliExecutionPlanV1 { + primary_authority: MycCliPrimaryAuthorityV1::Offline, + offline_operation: Some(operation), + admin_operation: None, + daemon_unavailable_offline_fallback: false, + } +} + +const fn admin_plan(operation: MycCliAdminOperationV1) -> MycCliExecutionPlanV1 { + MycCliExecutionPlanV1 { + primary_authority: MycCliPrimaryAuthorityV1::LiveUnixAdmin, + offline_operation: None, + admin_operation: Some(operation), + daemon_unavailable_offline_fallback: false, + } +} + +const fn read_only_admin_plan( + admin_operation: MycCliAdminOperationV1, + offline_operation: MycCliOfflineOperationV1, +) -> MycCliExecutionPlanV1 { + MycCliExecutionPlanV1 { + primary_authority: MycCliPrimaryAuthorityV1::LiveUnixAdmin, + offline_operation: Some(offline_operation), + admin_operation: Some(admin_operation), + daemon_unavailable_offline_fallback: true, + } +} + fn validate_bootstrap_paths( profile: MycBootstrapProfileV1, repo_local_root: Option<&Path>, diff --git a/src/lib.rs b/src/lib.rs @@ -43,8 +43,10 @@ pub use admin_v1::{ build_myc_admin_router, }; pub use cli_v1::{ - MycBootstrapProfileV1, MycCliInvocationV1, MycCliV1Error, MycCliV1ErrorKind, MycCommandV1, - MycConfigCommandV1, MycIdentityCommandV1, MycStateCommandV1, parse_myc_cli_v1_from, + MycBootstrapProfileV1, MycCliAdminOperationV1, MycCliExecutionPlanV1, MycCliInvocationV1, + MycCliOfflineOperationV1, MycCliPrimaryAuthorityV1, MycCliV1Error, MycCliV1ErrorKind, + MycCommandV1, MycConfigCommandV1, MycIdentityCommandV1, MycStateCommandV1, + parse_myc_cli_v1_from, plan_myc_cli_v1, }; pub use config_v1::{ MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES, MYC_CONFIG_SCHEMA, MYC_CONFIG_SCHEMA_VERSION, diff --git a/src/main.rs b/src/main.rs @@ -4,7 +4,8 @@ use std::process::ExitCode; fn main() -> ExitCode { match myc::parse_myc_cli_v1_from(std::env::args_os()) { - Ok(_) => { + Ok(invocation) => { + let _plan = myc::plan_myc_cli_v1(&invocation); eprintln!("myc: command execution is unavailable"); ExitCode::FAILURE } diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -119,6 +119,11 @@ fn implementation_modules_are_private_and_rustdoc_uses_the_reviewed_readme() { #[test] fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() { for required in [ + "pub struct myc::MycCliExecutionPlanV1", + "pub enum myc::MycCliPrimaryAuthorityV1", + "pub enum myc::MycCliOfflineOperationV1", + "pub enum myc::MycCliAdminOperationV1", + "pub const fn myc::plan_myc_cli_v1", "pub struct myc::MycAdminRequestDocument", "pub struct myc::MycAdminResponseDocument", "pub enum myc::MycAdminMethod", diff --git a/tests/services_hardening_cli.rs b/tests/services_hardening_cli.rs @@ -1,14 +1,19 @@ #![forbid(unsafe_code)] +use std::collections::BTreeSet; use std::error::Error; use std::path::Path; use myc::{ - INSTANCE_ID_MAX_BYTES, MycBootstrapProfileV1, MycCliV1ErrorKind, MycCommandV1, - MycConfigCommandV1, MycIdentityCommandV1, MycStateCommandV1, parse_myc_cli_v1_from, + INSTANCE_ID_MAX_BYTES, MycBootstrapProfileV1, MycCliAdminOperationV1, MycCliOfflineOperationV1, + MycCliPrimaryAuthorityV1, MycCliV1ErrorKind, MycCommandV1, MycConfigCommandV1, + MycIdentityCommandV1, MycStateCommandV1, parse_myc_cli_v1_from, plan_myc_cli_v1, }; const CLI_SOURCE: &str = include_str!("../src/cli_v1.rs"); +const MAIN_SOURCE: &str = include_str!("../src/main.rs"); +const OPERATOR_CONTRACT: &str = + include_str!("../contracts/services_hardening/operator_contract.v1.json"); fn base(command: &[&str]) -> Vec<String> { let mut arguments = vec![ @@ -130,6 +135,345 @@ fn root_api_exposes_validated_cross_bound_bootstrap_values() { } #[test] +fn every_command_has_one_exact_nonforgeable_execution_plan() { + let vectors = [ + ("run", vec!["run"], "daemon", None, None, false), + ( + "config init", + vec!["config", "init"], + "offline", + Some("config"), + None, + false, + ), + ( + "config validate", + vec!["config", "validate"], + "offline", + Some("config"), + None, + false, + ), + ( + "config show", + vec!["config", "show"], + "offline", + Some("config"), + None, + false, + ), + ( + "config schema", + vec!["config", "schema"], + "offline", + Some("config"), + None, + false, + ), + ( + "state init", + vec!["state", "init"], + "offline", + Some("state_exclusive"), + None, + false, + ), + ( + "state status", + vec!["state", "status"], + "live_unix_admin", + Some("state_read_only"), + Some("/v1/state/status"), + true, + ), + ( + "state backup", + vec!["state", "backup"], + "live_unix_admin", + Some("state_read_only"), + Some("/v1/state/backup"), + true, + ), + ( + "state restore", + vec!["state", "restore"], + "offline", + Some("state_exclusive"), + None, + false, + ), + ( + "state verify", + vec!["state", "verify"], + "offline", + Some("state_exclusive"), + None, + false, + ), + ( + "state migrate", + vec!["state", "migrate"], + "offline", + Some("state_exclusive"), + None, + false, + ), + ( + "identity init", + vec!["identity", "init"], + "offline", + Some("identity_exclusive"), + None, + false, + ), + ( + "identity status", + vec!["identity", "status"], + "live_unix_admin", + Some("identity_read_only"), + Some("/v1/identity/status"), + true, + ), + ( + "identity rekey", + vec!["identity", "rekey"], + "live_unix_admin", + None, + Some("/v1/identity/rekey"), + false, + ), + ( + "identity replace", + vec!["identity", "replace"], + "live_unix_admin", + None, + Some("/v1/identity/replace"), + false, + ), + ( + "identity export-public", + vec!["identity", "export-public"], + "live_unix_admin", + Some("identity_read_only"), + Some("/v1/identity/public"), + true, + ), + ( + "status", + vec!["status"], + "live_unix_admin", + Some("state_read_only"), + Some("/v1/status"), + true, + ), + ( + "doctor", + vec!["doctor"], + "offline", + Some("doctor"), + None, + false, + ), + ]; + let contract: serde_json::Value = + serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + let dispatch = contract + .get("cli_dispatch") + .and_then(serde_json::Value::as_object) + .expect("CLI dispatch contract"); + assert_eq!( + dispatch.keys().map(String::as_str).collect::<BTreeSet<_>>(), + BTreeSet::from([ + "commands", + "live_direct_sqlite_access", + "live_mutation_offline_fallback", + "parse_count", + "primary_authorities", + "read_only_offline_fallback_requires_free_daemon_writer_lock", + ]) + ); + assert_eq!( + dispatch + .get("parse_count") + .and_then(serde_json::Value::as_u64), + Some(1) + ); + assert_eq!( + dispatch + .get("live_direct_sqlite_access") + .and_then(serde_json::Value::as_bool), + Some(false) + ); + assert_eq!( + dispatch.get("primary_authorities"), + Some(&serde_json::json!(["daemon", "offline", "live_unix_admin"])) + ); + assert_eq!( + dispatch + .get("read_only_offline_fallback_requires_free_daemon_writer_lock") + .and_then(serde_json::Value::as_bool), + Some(true) + ); + assert_eq!( + dispatch + .get("live_mutation_offline_fallback") + .and_then(serde_json::Value::as_bool), + Some(false) + ); + let commands = dispatch + .get("commands") + .and_then(serde_json::Value::as_array) + .expect("command inventory"); + assert_eq!(commands.len(), vectors.len()); + + for (index, (command, arguments, authority, offline, route, fallback)) in + vectors.into_iter().enumerate() + { + let invocation = parse_myc_cli_v1_from(base(&arguments)).expect("command"); + let plan = plan_myc_cli_v1(&invocation); + assert_eq!(authority_name(plan.primary_authority()), authority); + assert_eq!(plan.offline_operation().map(offline_name), offline); + assert_eq!(plan.admin_operation().map(admin_path), route); + assert_eq!(plan.allows_daemon_unavailable_offline_fallback(), fallback); + + let row = commands[index].as_object().expect("command row"); + let mut expected_keys = BTreeSet::from(["command", "primary_authority"]); + if offline.is_some() { + expected_keys.insert("offline_operation"); + } + if route.is_some() { + expected_keys.insert("admin_route"); + } + if fallback { + expected_keys.insert("daemon_unavailable_offline_fallback"); + } + assert_eq!( + row.keys().map(String::as_str).collect::<BTreeSet<_>>(), + expected_keys + ); + assert_eq!( + row.get("command").and_then(serde_json::Value::as_str), + Some(command) + ); + assert_eq!( + row.get("primary_authority") + .and_then(serde_json::Value::as_str), + Some(authority) + ); + assert_eq!( + row.get("offline_operation") + .and_then(serde_json::Value::as_str), + offline + ); + assert_eq!( + row.get("admin_route").and_then(serde_json::Value::as_str), + route + ); + assert_eq!( + row.get("daemon_unavailable_offline_fallback") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + fallback + ); + } +} + +fn authority_name(authority: MycCliPrimaryAuthorityV1) -> &'static str { + match authority { + MycCliPrimaryAuthorityV1::Daemon => "daemon", + MycCliPrimaryAuthorityV1::Offline => "offline", + MycCliPrimaryAuthorityV1::LiveUnixAdmin => "live_unix_admin", + } +} + +fn offline_name(operation: MycCliOfflineOperationV1) -> &'static str { + match operation { + MycCliOfflineOperationV1::Config => "config", + MycCliOfflineOperationV1::StateExclusive => "state_exclusive", + MycCliOfflineOperationV1::StateReadOnly => "state_read_only", + MycCliOfflineOperationV1::IdentityExclusive => "identity_exclusive", + MycCliOfflineOperationV1::IdentityReadOnly => "identity_read_only", + MycCliOfflineOperationV1::Doctor => "doctor", + } +} + +fn admin_path(operation: MycCliAdminOperationV1) -> &'static str { + match operation { + MycCliAdminOperationV1::Status => "/v1/status", + MycCliAdminOperationV1::StateStatus => "/v1/state/status", + MycCliAdminOperationV1::StateBackup => "/v1/state/backup", + MycCliAdminOperationV1::IdentityStatus => "/v1/identity/status", + MycCliAdminOperationV1::IdentityRekey => "/v1/identity/rekey", + MycCliAdminOperationV1::IdentityReplace => "/v1/identity/replace", + MycCliAdminOperationV1::IdentityPublic => "/v1/identity/public", + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[test] +fn cli_admin_operations_match_the_governed_route_inventory() { + use myc::MycAdminRoute; + + let vectors = [ + (MycCliAdminOperationV1::Status, MycAdminRoute::Status), + ( + MycCliAdminOperationV1::StateStatus, + MycAdminRoute::StateStatus, + ), + ( + MycCliAdminOperationV1::StateBackup, + MycAdminRoute::StateBackup, + ), + ( + MycCliAdminOperationV1::IdentityStatus, + MycAdminRoute::IdentityStatus, + ), + ( + MycCliAdminOperationV1::IdentityRekey, + MycAdminRoute::IdentityRekey, + ), + ( + MycCliAdminOperationV1::IdentityReplace, + MycAdminRoute::IdentityReplace, + ), + ( + MycCliAdminOperationV1::IdentityPublic, + MycAdminRoute::IdentityPublic, + ), + ]; + for (operation, route) in vectors { + assert_eq!(operation.route(), route); + assert_eq!(admin_path(operation), route.path()); + } +} + +#[test] +fn execution_plan_debug_retains_no_bootstrap_or_path_values() { + let invocation = parse_myc_cli_v1_from([ + "myc", + "--profile", + "repo-local", + "--instance", + "secret-instance", + "--repo-local-root", + "/secret/repository", + "--config", + "/secret/config.toml", + "identity", + "rekey", + ]) + .expect("valid invocation"); + let rendered = format!("{invocation:?} {:?}", plan_myc_cli_v1(&invocation)); + for forbidden in [ + "secret-instance", + "/secret/repository", + "/secret/config.toml", + ] { + assert!(!rendered.contains(forbidden)); + } +} + +#[test] fn root_api_rejects_prototype_and_arbitrary_leaf_arguments_safely() { for arguments in [ base(&["--env-file", "/secret/config.env", "run"]), @@ -157,10 +501,19 @@ fn parser_is_single_pass_pure_and_privately_implemented() { "serde_json::", "toml::", "pub mod cli_v1", + "sqlx::", + "open_myc_state_", + "MycStateHost", ] { assert!(!CLI_SOURCE.contains(forbidden), "found `{forbidden}`"); } + assert_eq!(MAIN_SOURCE.matches("parse_myc_cli_v1_from").count(), 1); + assert_eq!(MAIN_SOURCE.matches("plan_myc_cli_v1").count(), 1); + for forbidden in ["sqlx::", "open_myc_state_", "MycStateHost"] { + assert!(!MAIN_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;"));