rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit b27c561a885b5904d270848eed5c71d8855fc7e4
parent d2ca795a81de1b4f9c6200f06b38d6723feabc58
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 17:15:55 +0000

cli: freeze RHI one-pass execution authority

- bind all 31 commands to one sealed daemon, offline, or Unix-admin plan
- forbid live offline and direct-SQLite fallback in the machine contract
- qualify exact route mapping, API closure, and single parse/plan use

Diffstat:
MAGENTS.md | 8++++++++
MREADME | 9+++++++--
Mcontracts/api_baselines/rhi.txt | 40++++++++++++++++++++++++++++++++++++++++
Mcontracts/services_hardening/operator_contract.v1.json | 39+++++++++++++++++++++++++++++++++++++++
Msrc/cli_v1.rs | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 5+++--
Msrc/main.rs | 3++-
Mtests/package_boundary.rs | 10++++++++++
Mtests/services_hardening_cli.rs | 399++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
9 files changed, 740 insertions(+), 9 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -413,6 +413,14 @@ use the exact lowercase-hex trade-ID type. Identity rotation remains offline create-new plus validated configuration apply and restart; Unix peer admission grants no direct SQLite or identity-provider mutation authority. +- Step 210 freezes the one-parse CLI execution split. Every admitted command + selects exactly daemon, offline, or permissioned Unix-admin authority. All + twenty live commands map one-to-one to the final twenty-route inventory and + carry no offline or direct-SQLite fallback. Config initialization, + validation, schema, and apply; exclusive state maintenance; initial identity + provisioning; and doctor remain offline. Do not reparse process arguments or + let a live CLI plan obtain SQLite, identity-provider, source, relay, task, + signal, or runtime authority. - Optional TCP operations expose only cached `/livez`, `/readyz`, and `/metrics`; requests must not perform SQLite, source, relay, DNS, identity, evidence, or credential probes. diff --git a/README b/README @@ -542,8 +542,13 @@ identity init, status, and public export; service status and metrics snapshot; reconciliation status, jobs, and refresh; source listing; trade projection and report queries; publication backlog, targets, and retry; desired-presence inspection, render, and refresh; and doctor. The parser performs no command -execution. Later ordered steps bind exact command inputs and each offline, -daemon, or live Unix-admin authority. +execution. The admitted invocation projects once into a sealed execution plan: +`run` selects daemon authority; config initialization, validation, schema, and +apply, exclusive state maintenance, initial identity provisioning, and doctor +select offline authority; and the remaining twenty commands map one-to-one to +the final twenty permissioned Unix-admin routes. No live command carries an +offline or direct-SQLite fallback. Later ordered steps bind each command's exact +input and execute the already-selected authority without reparsing arguments. Ordinary `run` accepts no identity-generation, identity-path, log-path, database-path, worker, environment-file, or arbitrary path-leaf flag. Identity diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt @@ -83,9 +83,41 @@ pub enum rhi::RhiBootstrapProfileV1 pub rhi::RhiBootstrapProfileV1::Interactive pub rhi::RhiBootstrapProfileV1::RepoLocal pub rhi::RhiBootstrapProfileV1::ServiceHost +pub enum rhi::RhiCliAdminOperationV1 +pub rhi::RhiCliAdminOperationV1::EffectiveConfig +pub rhi::RhiCliAdminOperationV1::IdentityPublic +pub rhi::RhiCliAdminOperationV1::IdentityStatus +pub rhi::RhiCliAdminOperationV1::MetricsSnapshot +pub rhi::RhiCliAdminOperationV1::PresenceDesired +pub rhi::RhiCliAdminOperationV1::PresenceRefresh +pub rhi::RhiCliAdminOperationV1::PresenceRender +pub rhi::RhiCliAdminOperationV1::PublicationBacklog +pub rhi::RhiCliAdminOperationV1::PublicationRetry +pub rhi::RhiCliAdminOperationV1::PublicationTargets +pub rhi::RhiCliAdminOperationV1::ReconciliationJobs +pub rhi::RhiCliAdminOperationV1::ReconciliationRefresh +pub rhi::RhiCliAdminOperationV1::ReconciliationStatus +pub rhi::RhiCliAdminOperationV1::Sources +pub rhi::RhiCliAdminOperationV1::StateBackup +pub rhi::RhiCliAdminOperationV1::StateStatus +pub rhi::RhiCliAdminOperationV1::Status +pub rhi::RhiCliAdminOperationV1::TradeProjection +pub rhi::RhiCliAdminOperationV1::TradeReportCurrent +pub rhi::RhiCliAdminOperationV1::TradeReports +impl rhi::RhiCliAdminOperationV1 +pub const fn rhi::RhiCliAdminOperationV1::route(self) -> rhi::RhiAdminRoute +pub enum rhi::RhiCliOfflineOperationV1 +pub rhi::RhiCliOfflineOperationV1::Config +pub rhi::RhiCliOfflineOperationV1::Doctor +pub rhi::RhiCliOfflineOperationV1::IdentityExclusive +pub rhi::RhiCliOfflineOperationV1::StateExclusive pub enum rhi::RhiCliOutputModeV1 pub rhi::RhiCliOutputModeV1::Human pub rhi::RhiCliOutputModeV1::Json +pub enum rhi::RhiCliPrimaryAuthorityV1 +pub rhi::RhiCliPrimaryAuthorityV1::Daemon +pub rhi::RhiCliPrimaryAuthorityV1::LiveUnixAdmin +pub rhi::RhiCliPrimaryAuthorityV1::Offline pub enum rhi::RhiCliV1ErrorKind pub rhi::RhiCliV1ErrorKind::InvalidArguments pub rhi::RhiCliV1ErrorKind::InvalidConfigPath @@ -735,6 +767,13 @@ impl rhi::RhiBoundAdminServer pub async fn rhi::RhiBoundAdminServer::serve(self, rhi::RhiAdminCancellationToken) -> core::result::Result<(), rhi::RhiAdminServerError> impl core::fmt::Debug for rhi::RhiBoundAdminServer pub fn rhi::RhiBoundAdminServer::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiCliExecutionPlanV1 +impl rhi::RhiCliExecutionPlanV1 +pub const fn rhi::RhiCliExecutionPlanV1::admin_operation(&self) -> core::option::Option<rhi::RhiCliAdminOperationV1> +pub const fn rhi::RhiCliExecutionPlanV1::offline_operation(&self) -> core::option::Option<rhi::RhiCliOfflineOperationV1> +pub const fn rhi::RhiCliExecutionPlanV1::primary_authority(&self) -> rhi::RhiCliPrimaryAuthorityV1 +impl core::fmt::Debug for rhi::RhiCliExecutionPlanV1 +pub fn rhi::RhiCliExecutionPlanV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct rhi::RhiCliInvocationV1 impl rhi::RhiCliInvocationV1 pub const fn rhi::RhiCliInvocationV1::command(&self) -> rhi::RhiCommandV1 @@ -2023,6 +2062,7 @@ pub async fn rhi::open_rhi_state_read_write(&rhi::RhiRuntimeContext, &rhi::RhiSt pub async fn rhi::open_rhi_state_read_write_from_config(&rhi::RhiRuntimeContext, &rhi::RhiConfigDocumentV1, radroots_service_sqlite::migration::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<rhi::RhiStateHost, rhi::RhiStateHostError> pub fn rhi::parse_rhi_cli_v1_from<I, T>(I) -> core::result::Result<rhi::RhiCliInvocationV1, rhi::RhiCliV1Error> where I: core::iter::traits::collect::IntoIterator<Item = T>, T: core::convert::Into<std::ffi::os_str::OsString> + core::clone::Clone pub fn rhi::parse_rhi_config_v1(&[u8], rhi::RhiConfigProfile) -> core::result::Result<rhi::RhiConfigDocumentV1, rhi::RhiConfigV1Error> +pub const fn rhi::plan_rhi_cli_v1(&rhi::RhiCliInvocationV1) -> rhi::RhiCliExecutionPlanV1 pub fn rhi::provision_rhi_encrypted_identity(&rhi::RhiIdentityEnvelopeBinding, &rhi::RhiWrappingCredential, rhi::RhiEncryptedIdentityProvisioningMaterial) -> core::result::Result<rhi::RhiDecryptedIdentity, rhi::RhiEncryptedIdentityEnvelopeError> pub fn rhi::reduce_rhi_reconciliation_manifest(rhi::RhiReconciliationManifest) -> core::result::Result<rhi::RhiReconciliationProjection, rhi::RhiReconciliationReducerError> pub fn rhi::resolve_rhi_runtime_context(&radroots_runtime_paths::roots::RadrootsPathResolver, &rhi::RhiCliInvocationV1) -> core::result::Result<rhi::RhiRuntimeContext, rhi::RhiRuntimeContextError> diff --git a/contracts/services_hardening/operator_contract.v1.json b/contracts/services_hardening/operator_contract.v1.json @@ -208,6 +208,45 @@ } } }, + "cli_dispatch": { + "parse_count": 1, + "primary_authorities": ["daemon", "offline", "live_unix_admin"], + "live_direct_sqlite_access": false, + "live_command_offline_fallback": false, + "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": "live_unix_admin", "admin_route": "/v1/config/effective" }, + { "command": "config schema", "primary_authority": "offline", "offline_operation": "config" }, + { "command": "config apply", "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" }, + { "command": "state backup", "primary_authority": "live_unix_admin", "admin_route": "/v1/state/backup" }, + { "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" }, + { "command": "identity export-public", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/public" }, + { "command": "status", "primary_authority": "live_unix_admin", "admin_route": "/v1/status" }, + { "command": "metrics snapshot", "primary_authority": "live_unix_admin", "admin_route": "/v1/metrics/snapshot" }, + { "command": "reconciliation status", "primary_authority": "live_unix_admin", "admin_route": "/v1/reconciliation/status" }, + { "command": "reconciliation jobs", "primary_authority": "live_unix_admin", "admin_route": "/v1/reconciliation/jobs" }, + { "command": "reconciliation refresh", "primary_authority": "live_unix_admin", "admin_route": "/v1/reconciliation/refresh" }, + { "command": "sources list", "primary_authority": "live_unix_admin", "admin_route": "/v1/sources" }, + { "command": "trade projection", "primary_authority": "live_unix_admin", "admin_route": "/v1/trades/{trade_id}/projection" }, + { "command": "trade report-current", "primary_authority": "live_unix_admin", "admin_route": "/v1/trades/{trade_id}/reports/current" }, + { "command": "trade reports", "primary_authority": "live_unix_admin", "admin_route": "/v1/trades/{trade_id}/reports" }, + { "command": "publication backlog", "primary_authority": "live_unix_admin", "admin_route": "/v1/publication/backlog" }, + { "command": "publication targets", "primary_authority": "live_unix_admin", "admin_route": "/v1/publication/targets" }, + { "command": "publication retry", "primary_authority": "live_unix_admin", "admin_route": "/v1/publication/retry" }, + { "command": "presence desired", "primary_authority": "live_unix_admin", "admin_route": "/v1/presence/desired" }, + { "command": "presence render", "primary_authority": "live_unix_admin", "admin_route": "/v1/presence/render" }, + { "command": "presence refresh", "primary_authority": "live_unix_admin", "admin_route": "/v1/presence/refresh" }, + { "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 @@ -114,6 +114,131 @@ pub enum RhiPresenceCommandV1 { Refresh, } +/// The only three process authorities selected by the hardened CLI. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiCliPrimaryAuthorityV1 { + Daemon, + Offline, + LiveUnixAdmin, +} + +/// The closed offline operation classes selected before any state access. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiCliOfflineOperationV1 { + Config, + StateExclusive, + IdentityExclusive, + Doctor, +} + +/// The closed Unix-admin operations reachable from the command inventory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiCliAdminOperationV1 { + Status, + EffectiveConfig, + IdentityStatus, + IdentityPublic, + StateStatus, + StateBackup, + MetricsSnapshot, + ReconciliationStatus, + ReconciliationJobs, + ReconciliationRefresh, + Sources, + TradeProjection, + TradeReportCurrent, + TradeReports, + PublicationBacklog, + PublicationTargets, + PublicationRetry, + PresenceDesired, + PresenceRender, + PresenceRefresh, +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +impl RhiCliAdminOperationV1 { + /// Returns the exact native Unix-admin route selected by this operation. + #[must_use] + pub const fn route(self) -> crate::RhiAdminRoute { + match self { + Self::Status => crate::RhiAdminRoute::Status, + Self::EffectiveConfig => crate::RhiAdminRoute::EffectiveConfig, + Self::IdentityStatus => crate::RhiAdminRoute::IdentityStatus, + Self::IdentityPublic => crate::RhiAdminRoute::IdentityPublic, + Self::StateStatus => crate::RhiAdminRoute::StateStatus, + Self::StateBackup => crate::RhiAdminRoute::StateBackup, + Self::MetricsSnapshot => crate::RhiAdminRoute::MetricsSnapshot, + Self::ReconciliationStatus => crate::RhiAdminRoute::ReconciliationStatus, + Self::ReconciliationJobs => crate::RhiAdminRoute::ReconciliationJobs, + Self::ReconciliationRefresh => crate::RhiAdminRoute::ReconciliationRefresh, + Self::Sources => crate::RhiAdminRoute::Sources, + Self::TradeProjection => crate::RhiAdminRoute::TradeProjection, + Self::TradeReportCurrent => crate::RhiAdminRoute::TradeReportCurrent, + Self::TradeReports => crate::RhiAdminRoute::TradeReports, + Self::PublicationBacklog => crate::RhiAdminRoute::PublicationBacklog, + Self::PublicationTargets => crate::RhiAdminRoute::PublicationTargets, + Self::PublicationRetry => crate::RhiAdminRoute::PublicationRetry, + Self::PresenceDesired => crate::RhiAdminRoute::PresenceDesired, + Self::PresenceRender => crate::RhiAdminRoute::PresenceRender, + Self::PresenceRefresh => crate::RhiAdminRoute::PresenceRefresh, + } + } +} + +/// A sealed, side-effect-free execution plan for one admitted CLI invocation. +/// +/// Construction is owned by [`plan_rhi_cli_v1`]. Live commands carry only a +/// governed Unix-admin operation and never receive an offline or direct-SQLite +/// fallback. +/// +/// ```compile_fail +/// use rhi::{RhiCliExecutionPlanV1, RhiCliPrimaryAuthorityV1}; +/// +/// let _ = RhiCliExecutionPlanV1 { +/// primary_authority: RhiCliPrimaryAuthorityV1::Offline, +/// offline_operation: None, +/// admin_operation: None, +/// }; +/// ``` +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct RhiCliExecutionPlanV1 { + primary_authority: RhiCliPrimaryAuthorityV1, + offline_operation: Option<RhiCliOfflineOperationV1>, + admin_operation: Option<RhiCliAdminOperationV1>, +} + +impl RhiCliExecutionPlanV1 { + /// Returns the sole selected process authority. + #[must_use] + pub const fn primary_authority(&self) -> RhiCliPrimaryAuthorityV1 { + self.primary_authority + } + + /// Returns the bounded offline operation, when the plan admits one. + #[must_use] + pub const fn offline_operation(&self) -> Option<RhiCliOfflineOperationV1> { + self.offline_operation + } + + /// Returns the bounded Unix-admin operation, when the plan admits one. + #[must_use] + pub const fn admin_operation(&self) -> Option<RhiCliAdminOperationV1> { + self.admin_operation + } +} + +impl fmt::Debug for RhiCliExecutionPlanV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiCliExecutionPlanV1") + .field("primary_authority", &self.primary_authority) + .field("offline_operation", &self.offline_operation) + .field("admin_operation", &self.admin_operation) + .finish() + } +} + /// Stable source-free classification for command-line admission failures. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RhiCliV1ErrorKind { @@ -278,6 +403,117 @@ 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. No live command receives direct SQLite or offline fallback +/// authority. +#[must_use] +pub const fn plan_rhi_cli_v1(invocation: &RhiCliInvocationV1) -> RhiCliExecutionPlanV1 { + match invocation.command { + RhiCommandV1::Run => daemon_plan(), + RhiCommandV1::Config(RhiConfigCommandV1::Init) + | RhiCommandV1::Config(RhiConfigCommandV1::Validate) + | RhiCommandV1::Config(RhiConfigCommandV1::Schema) + | RhiCommandV1::Config(RhiConfigCommandV1::Apply) => { + offline_plan(RhiCliOfflineOperationV1::Config) + } + RhiCommandV1::Config(RhiConfigCommandV1::Show) => { + admin_plan(RhiCliAdminOperationV1::EffectiveConfig) + } + RhiCommandV1::State(RhiStateCommandV1::Init) + | RhiCommandV1::State(RhiStateCommandV1::Restore) + | RhiCommandV1::State(RhiStateCommandV1::Verify) + | RhiCommandV1::State(RhiStateCommandV1::Migrate) => { + offline_plan(RhiCliOfflineOperationV1::StateExclusive) + } + RhiCommandV1::State(RhiStateCommandV1::Status) => { + admin_plan(RhiCliAdminOperationV1::StateStatus) + } + RhiCommandV1::State(RhiStateCommandV1::Backup) => { + admin_plan(RhiCliAdminOperationV1::StateBackup) + } + RhiCommandV1::Identity(RhiIdentityCommandV1::Init) => { + offline_plan(RhiCliOfflineOperationV1::IdentityExclusive) + } + RhiCommandV1::Identity(RhiIdentityCommandV1::Status) => { + admin_plan(RhiCliAdminOperationV1::IdentityStatus) + } + RhiCommandV1::Identity(RhiIdentityCommandV1::ExportPublic) => { + admin_plan(RhiCliAdminOperationV1::IdentityPublic) + } + RhiCommandV1::Status => admin_plan(RhiCliAdminOperationV1::Status), + RhiCommandV1::Metrics(RhiMetricsCommandV1::Snapshot) => { + admin_plan(RhiCliAdminOperationV1::MetricsSnapshot) + } + RhiCommandV1::Reconciliation(RhiReconciliationCommandV1::Status) => { + admin_plan(RhiCliAdminOperationV1::ReconciliationStatus) + } + RhiCommandV1::Reconciliation(RhiReconciliationCommandV1::Jobs) => { + admin_plan(RhiCliAdminOperationV1::ReconciliationJobs) + } + RhiCommandV1::Reconciliation(RhiReconciliationCommandV1::Refresh) => { + admin_plan(RhiCliAdminOperationV1::ReconciliationRefresh) + } + RhiCommandV1::Sources(RhiSourcesCommandV1::List) => { + admin_plan(RhiCliAdminOperationV1::Sources) + } + RhiCommandV1::Trade(RhiTradeCommandV1::Projection) => { + admin_plan(RhiCliAdminOperationV1::TradeProjection) + } + RhiCommandV1::Trade(RhiTradeCommandV1::ReportCurrent) => { + admin_plan(RhiCliAdminOperationV1::TradeReportCurrent) + } + RhiCommandV1::Trade(RhiTradeCommandV1::Reports) => { + admin_plan(RhiCliAdminOperationV1::TradeReports) + } + RhiCommandV1::Publication(RhiPublicationCommandV1::Backlog) => { + admin_plan(RhiCliAdminOperationV1::PublicationBacklog) + } + RhiCommandV1::Publication(RhiPublicationCommandV1::Targets) => { + admin_plan(RhiCliAdminOperationV1::PublicationTargets) + } + RhiCommandV1::Publication(RhiPublicationCommandV1::Retry) => { + admin_plan(RhiCliAdminOperationV1::PublicationRetry) + } + RhiCommandV1::Presence(RhiPresenceCommandV1::Desired) => { + admin_plan(RhiCliAdminOperationV1::PresenceDesired) + } + RhiCommandV1::Presence(RhiPresenceCommandV1::Render) => { + admin_plan(RhiCliAdminOperationV1::PresenceRender) + } + RhiCommandV1::Presence(RhiPresenceCommandV1::Refresh) => { + admin_plan(RhiCliAdminOperationV1::PresenceRefresh) + } + RhiCommandV1::Doctor => offline_plan(RhiCliOfflineOperationV1::Doctor), + } +} + +const fn daemon_plan() -> RhiCliExecutionPlanV1 { + RhiCliExecutionPlanV1 { + primary_authority: RhiCliPrimaryAuthorityV1::Daemon, + offline_operation: None, + admin_operation: None, + } +} + +const fn offline_plan(operation: RhiCliOfflineOperationV1) -> RhiCliExecutionPlanV1 { + RhiCliExecutionPlanV1 { + primary_authority: RhiCliPrimaryAuthorityV1::Offline, + offline_operation: Some(operation), + admin_operation: None, + } +} + +const fn admin_plan(operation: RhiCliAdminOperationV1) -> RhiCliExecutionPlanV1 { + RhiCliExecutionPlanV1 { + primary_authority: RhiCliPrimaryAuthorityV1::LiveUnixAdmin, + offline_operation: None, + admin_operation: Some(operation), + } +} + fn validate_bootstrap_paths( profile: RhiBootstrapProfileV1, repo_local_root: Option<&Path>, diff --git a/src/lib.rs b/src/lib.rs @@ -46,10 +46,11 @@ pub use admin_v1::{ RhiBoundAdminServer, build_rhi_admin_router, }; pub use cli_v1::{ - RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1, RhiCliV1Error, + RhiBootstrapProfileV1, RhiCliAdminOperationV1, RhiCliExecutionPlanV1, RhiCliInvocationV1, + RhiCliOfflineOperationV1, RhiCliOutputModeV1, RhiCliPrimaryAuthorityV1, RhiCliV1Error, RhiCliV1ErrorKind, RhiCommandV1, RhiConfigCommandV1, RhiIdentityCommandV1, RhiMetricsCommandV1, RhiPresenceCommandV1, RhiPublicationCommandV1, RhiReconciliationCommandV1, RhiSourcesCommandV1, - RhiStateCommandV1, RhiTradeCommandV1, parse_rhi_cli_v1_from, + RhiStateCommandV1, RhiTradeCommandV1, parse_rhi_cli_v1_from, plan_rhi_cli_v1, }; pub use config_v1::{ RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES, RHI_CONFIG_EFFECTIVE_MAX_UTF8_BYTES, RHI_CONFIG_SCHEMA, diff --git a/src/main.rs b/src/main.rs @@ -5,7 +5,7 @@ use std::process::ExitCode; use rhi::{ RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from, - resolve_rhi_runtime_context, + plan_rhi_cli_v1, resolve_rhi_runtime_context, }; fn main() -> ExitCode { @@ -27,6 +27,7 @@ fn exit_code_from_run(result: Result<(), ()>) -> ExitCode { } fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<(), ()> { + let _plan = plan_rhi_cli_v1(&invocation); let resolver = RadrootsPathResolver::new(RadrootsPlatform::current(), host_environment()); let _context = resolve_rhi_runtime_context(&resolver, &invocation).map_err(|_| ())?; Err(()) diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -242,6 +242,11 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { "RhiAdminServer", "RhiBoundAdminServer", "build_rhi_admin_router", + "RhiCliPrimaryAuthorityV1", + "RhiCliOfflineOperationV1", + "RhiCliAdminOperationV1", + "RhiCliExecutionPlanV1", + "plan_rhi_cli_v1", "RhiPublicationErrorKind", "RhiPublicationMode", "RhiPublicationRetryPolicy", @@ -356,6 +361,10 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { assert!(PUBLIC_API.contains("pub enum rhi::RhiPresenceDesiredErrorKind")); assert!(PUBLIC_API.contains("pub struct rhi::RhiSignedPresenceDocument")); assert!(PUBLIC_API.contains("pub enum rhi::RhiPresencePublicationErrorKind")); + assert!(PUBLIC_API.contains("pub struct rhi::RhiCliExecutionPlanV1")); + assert!(PUBLIC_API.contains( + "pub const fn rhi::plan_rhi_cli_v1(&rhi::RhiCliInvocationV1) -> rhi::RhiCliExecutionPlanV1" + )); assert!(!PUBLIC_API.contains("rhi::adapters::")); assert!(!PUBLIC_API.contains("rhi::features::")); assert!(!PUBLIC_API.contains("rhi::runtime_adapters::")); @@ -365,6 +374,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { assert!(!PUBLIC_API.contains("rhi::publication_submission::")); assert!(!PUBLIC_API.contains("rhi::presence_publication::")); assert!(!PUBLIC_API.contains("rhi::admin_v1::")); + assert!(!PUBLIC_API.contains("rhi::cli_v1::")); } #[test] diff --git a/tests/services_hardening_cli.rs b/tests/services_hardening_cli.rs @@ -1,15 +1,22 @@ #![forbid(unsafe_code)] +use std::collections::BTreeSet; use std::error::Error; use std::path::Path; use rhi::{ - INSTANCE_ID_MAX_BYTES, RhiBootstrapProfileV1, RhiCliOutputModeV1, RhiCliV1ErrorKind, - RhiCommandV1, RhiConfigCommandV1, RhiIdentityCommandV1, RhiMetricsCommandV1, - RhiPresenceCommandV1, RhiPublicationCommandV1, RhiReconciliationCommandV1, RhiSourcesCommandV1, - RhiStateCommandV1, RhiTradeCommandV1, parse_rhi_cli_v1_from, + INSTANCE_ID_MAX_BYTES, RhiBootstrapProfileV1, RhiCliAdminOperationV1, RhiCliOfflineOperationV1, + RhiCliOutputModeV1, RhiCliPrimaryAuthorityV1, RhiCliV1ErrorKind, RhiCommandV1, + RhiConfigCommandV1, RhiIdentityCommandV1, RhiMetricsCommandV1, RhiPresenceCommandV1, + RhiPublicationCommandV1, RhiReconciliationCommandV1, RhiSourcesCommandV1, RhiStateCommandV1, + RhiTradeCommandV1, parse_rhi_cli_v1_from, plan_rhi_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 parse(command: &[&str]) -> rhi::RhiCliInvocationV1 { let mut arguments = vec!["rhi", "--profile", "service-host", "--instance", "default"]; arguments.extend_from_slice(command); @@ -224,6 +231,390 @@ fn bootstrap_values_are_explicit_bounded_and_cross_bound() { } #[test] +fn every_command_has_one_exact_nonforgeable_execution_plan() { + let vectors = [ + ("run", vec!["run"], "daemon", None, None), + ( + "config init", + vec!["config", "init"], + "offline", + Some("config"), + None, + ), + ( + "config validate", + vec!["config", "validate"], + "offline", + Some("config"), + None, + ), + ( + "config show", + vec!["config", "show"], + "live_unix_admin", + None, + Some("/v1/config/effective"), + ), + ( + "config schema", + vec!["config", "schema"], + "offline", + Some("config"), + None, + ), + ( + "config apply", + vec!["config", "apply"], + "offline", + Some("config"), + None, + ), + ( + "state init", + vec!["state", "init"], + "offline", + Some("state_exclusive"), + None, + ), + ( + "state status", + vec!["state", "status"], + "live_unix_admin", + None, + Some("/v1/state/status"), + ), + ( + "state backup", + vec!["state", "backup"], + "live_unix_admin", + None, + Some("/v1/state/backup"), + ), + ( + "state restore", + vec!["state", "restore"], + "offline", + Some("state_exclusive"), + None, + ), + ( + "state verify", + vec!["state", "verify"], + "offline", + Some("state_exclusive"), + None, + ), + ( + "state migrate", + vec!["state", "migrate"], + "offline", + Some("state_exclusive"), + None, + ), + ( + "identity init", + vec!["identity", "init"], + "offline", + Some("identity_exclusive"), + None, + ), + ( + "identity status", + vec!["identity", "status"], + "live_unix_admin", + None, + Some("/v1/identity/status"), + ), + ( + "identity export-public", + vec!["identity", "export-public"], + "live_unix_admin", + None, + Some("/v1/identity/public"), + ), + ( + "status", + vec!["status"], + "live_unix_admin", + None, + Some("/v1/status"), + ), + ( + "metrics snapshot", + vec!["metrics", "snapshot"], + "live_unix_admin", + None, + Some("/v1/metrics/snapshot"), + ), + ( + "reconciliation status", + vec!["reconciliation", "status"], + "live_unix_admin", + None, + Some("/v1/reconciliation/status"), + ), + ( + "reconciliation jobs", + vec!["reconciliation", "jobs"], + "live_unix_admin", + None, + Some("/v1/reconciliation/jobs"), + ), + ( + "reconciliation refresh", + vec!["reconciliation", "refresh"], + "live_unix_admin", + None, + Some("/v1/reconciliation/refresh"), + ), + ( + "sources list", + vec!["sources", "list"], + "live_unix_admin", + None, + Some("/v1/sources"), + ), + ( + "trade projection", + vec!["trade", "projection"], + "live_unix_admin", + None, + Some("/v1/trades/{trade_id}/projection"), + ), + ( + "trade report-current", + vec!["trade", "report-current"], + "live_unix_admin", + None, + Some("/v1/trades/{trade_id}/reports/current"), + ), + ( + "trade reports", + vec!["trade", "reports"], + "live_unix_admin", + None, + Some("/v1/trades/{trade_id}/reports"), + ), + ( + "publication backlog", + vec!["publication", "backlog"], + "live_unix_admin", + None, + Some("/v1/publication/backlog"), + ), + ( + "publication targets", + vec!["publication", "targets"], + "live_unix_admin", + None, + Some("/v1/publication/targets"), + ), + ( + "publication retry", + vec!["publication", "retry"], + "live_unix_admin", + None, + Some("/v1/publication/retry"), + ), + ( + "presence desired", + vec!["presence", "desired"], + "live_unix_admin", + None, + Some("/v1/presence/desired"), + ), + ( + "presence render", + vec!["presence", "render"], + "live_unix_admin", + None, + Some("/v1/presence/render"), + ), + ( + "presence refresh", + vec!["presence", "refresh"], + "live_unix_admin", + None, + Some("/v1/presence/refresh"), + ), + ("doctor", vec!["doctor"], "offline", Some("doctor"), None), + ]; + 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_command_offline_fallback", + "live_direct_sqlite_access", + "parse_count", + "primary_authorities", + ]) + ); + assert_eq!(dispatch["parse_count"], 1); + assert_eq!( + dispatch["primary_authorities"], + serde_json::json!(["daemon", "offline", "live_unix_admin"]) + ); + assert_eq!(dispatch["live_direct_sqlite_access"], false); + assert_eq!(dispatch["live_command_offline_fallback"], false); + let commands = dispatch["commands"].as_array().expect("command inventory"); + assert_eq!(commands.len(), vectors.len()); + + for (index, (command, arguments, authority, offline, route)) in vectors.into_iter().enumerate() + { + let invocation = parse(&arguments); + let plan = plan_rhi_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); + + 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"); + } + assert_eq!( + row.keys().map(String::as_str).collect::<BTreeSet<_>>(), + expected_keys + ); + assert_eq!(row["command"], command); + assert_eq!(row["primary_authority"], 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 + ); + } +} + +fn authority_name(authority: RhiCliPrimaryAuthorityV1) -> &'static str { + match authority { + RhiCliPrimaryAuthorityV1::Daemon => "daemon", + RhiCliPrimaryAuthorityV1::Offline => "offline", + RhiCliPrimaryAuthorityV1::LiveUnixAdmin => "live_unix_admin", + } +} + +fn offline_name(operation: RhiCliOfflineOperationV1) -> &'static str { + match operation { + RhiCliOfflineOperationV1::Config => "config", + RhiCliOfflineOperationV1::StateExclusive => "state_exclusive", + RhiCliOfflineOperationV1::IdentityExclusive => "identity_exclusive", + RhiCliOfflineOperationV1::Doctor => "doctor", + } +} + +fn admin_path(operation: RhiCliAdminOperationV1) -> &'static str { + match operation { + RhiCliAdminOperationV1::Status => "/v1/status", + RhiCliAdminOperationV1::EffectiveConfig => "/v1/config/effective", + RhiCliAdminOperationV1::IdentityStatus => "/v1/identity/status", + RhiCliAdminOperationV1::IdentityPublic => "/v1/identity/public", + RhiCliAdminOperationV1::StateStatus => "/v1/state/status", + RhiCliAdminOperationV1::StateBackup => "/v1/state/backup", + RhiCliAdminOperationV1::MetricsSnapshot => "/v1/metrics/snapshot", + RhiCliAdminOperationV1::ReconciliationStatus => "/v1/reconciliation/status", + RhiCliAdminOperationV1::ReconciliationJobs => "/v1/reconciliation/jobs", + RhiCliAdminOperationV1::ReconciliationRefresh => "/v1/reconciliation/refresh", + RhiCliAdminOperationV1::Sources => "/v1/sources", + RhiCliAdminOperationV1::TradeProjection => "/v1/trades/{trade_id}/projection", + RhiCliAdminOperationV1::TradeReportCurrent => "/v1/trades/{trade_id}/reports/current", + RhiCliAdminOperationV1::TradeReports => "/v1/trades/{trade_id}/reports", + RhiCliAdminOperationV1::PublicationBacklog => "/v1/publication/backlog", + RhiCliAdminOperationV1::PublicationTargets => "/v1/publication/targets", + RhiCliAdminOperationV1::PublicationRetry => "/v1/publication/retry", + RhiCliAdminOperationV1::PresenceDesired => "/v1/presence/desired", + RhiCliAdminOperationV1::PresenceRender => "/v1/presence/render", + RhiCliAdminOperationV1::PresenceRefresh => "/v1/presence/refresh", + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[test] +fn cli_admin_operations_match_the_complete_governed_route_inventory() { + assert_eq!( + [ + RhiCliAdminOperationV1::Status, + RhiCliAdminOperationV1::EffectiveConfig, + RhiCliAdminOperationV1::IdentityStatus, + RhiCliAdminOperationV1::IdentityPublic, + RhiCliAdminOperationV1::StateStatus, + RhiCliAdminOperationV1::StateBackup, + RhiCliAdminOperationV1::MetricsSnapshot, + RhiCliAdminOperationV1::ReconciliationStatus, + RhiCliAdminOperationV1::ReconciliationJobs, + RhiCliAdminOperationV1::ReconciliationRefresh, + RhiCliAdminOperationV1::Sources, + RhiCliAdminOperationV1::TradeProjection, + RhiCliAdminOperationV1::TradeReportCurrent, + RhiCliAdminOperationV1::TradeReports, + RhiCliAdminOperationV1::PublicationBacklog, + RhiCliAdminOperationV1::PublicationTargets, + RhiCliAdminOperationV1::PublicationRetry, + RhiCliAdminOperationV1::PresenceDesired, + RhiCliAdminOperationV1::PresenceRender, + RhiCliAdminOperationV1::PresenceRefresh, + ] + .map(RhiCliAdminOperationV1::route), + rhi::RhiAdminRoute::ALL + ); +} + +#[test] +fn execution_plan_is_safe_and_the_binary_parses_and_plans_once() { + let invocation = parse_rhi_cli_v1_from([ + "rhi", + "--profile", + "repo-local", + "--instance", + "secret-instance", + "--repo-local-root", + "/secret/repository", + "--config", + "/secret/config.toml", + "publication", + "retry", + ]) + .expect("valid invocation"); + let rendered = format!("{invocation:?} {:?}", plan_rhi_cli_v1(&invocation)); + for forbidden in [ + "secret-instance", + "/secret/repository", + "/secret/config.toml", + ] { + assert!(!rendered.contains(forbidden)); + } + + assert_eq!( + MAIN_SOURCE + .matches("parse_rhi_cli_v1_from(std::env::args_os())") + .count(), + 1 + ); + assert_eq!( + MAIN_SOURCE.matches("plan_rhi_cli_v1(&invocation)").count(), + 1 + ); + for source in [CLI_SOURCE, MAIN_SOURCE] { + for forbidden in ["sqlx::", "RhiStateHost", "open_rhi_state_"] { + assert!(!source.contains(forbidden), "found `{forbidden}`"); + } + } +} + +#[test] fn prototype_and_unsafe_commands_are_absent_and_errors_are_safe() { for arguments in [ vec![