rhi

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

commit fb88b899b93a652e06a27719e51d1761870195de
parent 0726c6766fbbc0037ace1490d0f808e41739f354
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 16:06:08 +0000

refactor(rhi): bind common Unix admin routes

- seal the exact 22-route and 36-model RHI v1 vocabulary\n- register only the seven Step 206 common routes on the shared bounded Unix server\n- add strict wire, redaction, transport, and package-boundary qualification

Diffstat:
MAGENTS.md | 6++++++
MREADME | 27+++++++++++++++++++++++++--
Mcontracts/api_baselines/rhi.txt | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/services_hardening/admin_common.v1.json | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/admin_v1.rs | 2135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 8++++++++
Mtests/package_boundary.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++--
Atests/services_hardening_admin_common.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 2495 insertions(+), 4 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -398,6 +398,12 @@ safe confirmation for destructive or identity-sensitive work. Never unlink a live socket owner; remove only a proven stale socket under the resolved instance runtime directory. +- Keep the public RHI route and document vocabulary closed against the exact + operator contract. Register only routes owned by the current ordered step: + Step 206 owns the seven common routes, Step 207 owns domain routes, Step 208 + owns sensitive routes, and Step 209 alone may claim the complete 22-route + surface. Never expose the shared raw router, listener, JSON handler, or a + caller-selected socket path. - 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 @@ -550,6 +550,27 @@ database-path, worker, environment-file, or arbitrary path-leaf flag. Identity rekey and replacement are not commands; rotation is a create-new offline artifact plus governed configuration apply. +## Common Unix-admin boundary + +Step 206 binds the seven common RHI routes for detailed status, redacted +effective configuration, service-identity status and public export, state +status, online backup, and a bounded metrics snapshot to the shared +`radroots_service_host` HTTP/1.1-over-Unix server. The public RHI route and +document types are closed, response construction admits only bounded compact +canonical JSON matching the exact machine model, and public diagnostics retain +no path, request, identity, or dependency-owned cause. + +The server projects only the normalized configuration's admitted transport +limits, derives `admin.sock` only from the sealed runtime context, acquires the +shared writer authority before binding, and uses the shared server's system +entropy for absent correlation IDs. Construction does no I/O; binding does not +spawn; serving remains a later supervised runtime responsibility. Raw shared +routers, listeners, JSON values, and caller-selected socket paths never cross +the public RHI boundary. Domain and sensitive routes remain deliberately +unregistered until Steps 207 and 208, before Step 209 closes the complete +22-route/36-model inventory. The partial machine contract is +[`admin_common.v1.json`](contracts/services_hardening/admin_common.v1.json). + ## Sealed service-instance paths One validated CLI invocation resolves through `RhiRuntimeContext`, which owns @@ -579,8 +600,10 @@ schema-v2 configuration migration, schema-v3 immutable trade-evidence migration, schema-v4 source-checkpoint and dirty-generation migration, schema-v5 bounded reconciliation-job migration, schema-v6 immutable reconciliation-result migration, and schema-v7 immutable report, signed-event, -and publication-workflow migration. -Version seven contains the six shared immutable service-metadata and +publication-workflow migration. Schema v8 adds reconciliation-attempt replay +evidence, schema v9 adds durable presence desired state, and schema v10 adds +the exact-byte presence outbox, target, and attempt inventory. +Version ten contains the six shared immutable service-metadata and migration-ledger objects, the bounded append-only `rhi_config_bindings` table, separate immutable tables for canonical mutations, signed Nostr events, and accepted source observations, and generation-guarded relay checkpoints and diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt @@ -19,6 +19,66 @@ pub use rhi::TradeId pub use rhi::UnixTimeSeconds pub use rhi::WallClock pub use rhi::WallClockError +pub enum rhi::RhiAdminDocumentErrorKind +pub rhi::RhiAdminDocumentErrorKind::DuplicateField +pub rhi::RhiAdminDocumentErrorKind::InvalidModel +pub rhi::RhiAdminDocumentErrorKind::Malformed +pub rhi::RhiAdminDocumentErrorKind::NonCanonical +pub rhi::RhiAdminDocumentErrorKind::NullForbidden +pub rhi::RhiAdminDocumentErrorKind::TooLarge +pub enum rhi::RhiAdminHandlerErrorKind +pub rhi::RhiAdminHandlerErrorKind::Conflict +pub rhi::RhiAdminHandlerErrorKind::Internal +pub rhi::RhiAdminHandlerErrorKind::InvalidCursor +pub rhi::RhiAdminHandlerErrorKind::NotFound +pub rhi::RhiAdminHandlerErrorKind::OperationIdConflict +pub rhi::RhiAdminHandlerErrorKind::Unavailable +pub enum rhi::RhiAdminMethod +pub rhi::RhiAdminMethod::Get +pub rhi::RhiAdminMethod::Post +pub enum rhi::RhiAdminRoute +pub rhi::RhiAdminRoute::EffectiveConfig +pub rhi::RhiAdminRoute::IdentityPublic +pub rhi::RhiAdminRoute::IdentityRekey +pub rhi::RhiAdminRoute::IdentityReplace +pub rhi::RhiAdminRoute::IdentityStatus +pub rhi::RhiAdminRoute::MetricsSnapshot +pub rhi::RhiAdminRoute::PresenceDesired +pub rhi::RhiAdminRoute::PresenceRefresh +pub rhi::RhiAdminRoute::PresenceRender +pub rhi::RhiAdminRoute::PublicationBacklog +pub rhi::RhiAdminRoute::PublicationRetry +pub rhi::RhiAdminRoute::PublicationTargets +pub rhi::RhiAdminRoute::ReconciliationJobs +pub rhi::RhiAdminRoute::ReconciliationRefresh +pub rhi::RhiAdminRoute::ReconciliationStatus +pub rhi::RhiAdminRoute::Sources +pub rhi::RhiAdminRoute::StateBackup +pub rhi::RhiAdminRoute::StateStatus +pub rhi::RhiAdminRoute::Status +pub rhi::RhiAdminRoute::TradeProjection +pub rhi::RhiAdminRoute::TradeReportCurrent +pub rhi::RhiAdminRoute::TradeReports +impl rhi::RhiAdminRoute +pub const rhi::RhiAdminRoute::ALL: [Self; 22] +pub const rhi::RhiAdminRoute::COMMON: [Self; 7] +pub const fn rhi::RhiAdminRoute::is_mutation(self) -> bool +pub const fn rhi::RhiAdminRoute::method(self) -> rhi::RhiAdminMethod +pub const fn rhi::RhiAdminRoute::operation_id(self) -> &'static str +pub const fn rhi::RhiAdminRoute::path(self) -> &'static str +pub const fn rhi::RhiAdminRoute::request_model(self) -> &'static str +pub const fn rhi::RhiAdminRoute::response_model(self) -> &'static str +pub enum rhi::RhiAdminServerErrorKind +pub rhi::RhiAdminServerErrorKind::Accept +pub rhi::RhiAdminServerErrorKind::Bind +pub rhi::RhiAdminServerErrorKind::ConnectionTaskPanicked +pub rhi::RhiAdminServerErrorKind::InvalidConfiguration +pub rhi::RhiAdminServerErrorKind::Listener +pub rhi::RhiAdminServerErrorKind::Router +pub rhi::RhiAdminServerErrorKind::ServerConfiguration +pub rhi::RhiAdminServerErrorKind::WriterAuthority +impl rhi::RhiAdminServerErrorKind +pub const fn rhi::RhiAdminServerErrorKind::code(self) -> &'static str pub enum rhi::RhiBootstrapProfileV1 pub rhi::RhiBootstrapProfileV1::Interactive pub rhi::RhiBootstrapProfileV1::RepoLocal @@ -601,6 +661,55 @@ pub fn rhi::NostrEventAdapter<'a>::raw_author(&'a self) -> alloc::string::String pub fn rhi::NostrEventAdapter<'a>::raw_content(&'a self) -> &'a str pub fn rhi::NostrEventAdapter<'a>::raw_id(&'a self) -> alloc::string::String pub fn rhi::NostrEventAdapter<'a>::raw_kind(&'a self) -> u32 +pub struct rhi::RhiAdminCancellationToken +impl rhi::RhiAdminCancellationToken +pub fn rhi::RhiAdminCancellationToken::cancel(&self) +pub fn rhi::RhiAdminCancellationToken::is_cancelled(&self) -> bool +pub fn rhi::RhiAdminCancellationToken::new() -> Self +impl core::fmt::Debug for rhi::RhiAdminCancellationToken +pub fn rhi::RhiAdminCancellationToken::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiAdminDocumentError +impl rhi::RhiAdminDocumentError +pub const fn rhi::RhiAdminDocumentError::kind(self) -> rhi::RhiAdminDocumentErrorKind +impl core::error::Error for rhi::RhiAdminDocumentError +impl core::fmt::Display for rhi::RhiAdminDocumentError +pub fn rhi::RhiAdminDocumentError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiAdminHandlerError +impl rhi::RhiAdminHandlerError +pub const fn rhi::RhiAdminHandlerError::kind(self) -> rhi::RhiAdminHandlerErrorKind +pub const fn rhi::RhiAdminHandlerError::new(rhi::RhiAdminHandlerErrorKind) -> Self +impl core::error::Error for rhi::RhiAdminHandlerError +impl core::fmt::Display for rhi::RhiAdminHandlerError +pub fn rhi::RhiAdminHandlerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiAdminRequestDocument +impl rhi::RhiAdminRequestDocument +pub fn rhi::RhiAdminRequestDocument::correlation_id(&self) -> &str +pub fn rhi::RhiAdminRequestDocument::model_bytes(&self) -> &[u8] +pub fn rhi::RhiAdminRequestDocument::operation_id(&self) -> core::option::Option<&str> +pub fn rhi::RhiAdminRequestDocument::parameter(&self, &str) -> core::option::Option<&str> +pub const fn rhi::RhiAdminRequestDocument::route(&self) -> rhi::RhiAdminRoute +impl core::fmt::Debug for rhi::RhiAdminRequestDocument +pub fn rhi::RhiAdminRequestDocument::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiAdminResponseDocument +impl rhi::RhiAdminResponseDocument +pub fn rhi::RhiAdminResponseDocument::canonical_bytes(&self) -> &[u8] +pub fn rhi::RhiAdminResponseDocument::from_canonical_bytes(rhi::RhiAdminRoute, &[u8]) -> core::result::Result<Self, rhi::RhiAdminDocumentError> +pub const fn rhi::RhiAdminResponseDocument::route(&self) -> rhi::RhiAdminRoute +impl core::fmt::Debug for rhi::RhiAdminResponseDocument +pub fn rhi::RhiAdminResponseDocument::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiAdminRouterError +impl core::error::Error for rhi::RhiAdminRouterError +impl core::fmt::Display for rhi::RhiAdminRouterError +pub fn rhi::RhiAdminRouterError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiAdminServerError +impl rhi::RhiAdminServerError +pub const fn rhi::RhiAdminServerError::code(self) -> &'static str +pub const fn rhi::RhiAdminServerError::kind(self) -> rhi::RhiAdminServerErrorKind +impl core::error::Error for rhi::RhiAdminServerError +impl core::fmt::Debug for rhi::RhiAdminServerError +pub fn rhi::RhiAdminServerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for rhi::RhiAdminServerError +pub fn rhi::RhiAdminServerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct rhi::RhiAdmittedTradeMutationEvent impl rhi::RhiAdmittedTradeMutationEvent pub fn rhi::RhiAdmittedTradeMutationEvent::authored_at_unix_seconds(&self) -> u64 @@ -612,6 +721,11 @@ pub const fn rhi::RhiAdmittedTradeMutationEvent::observed_at_unix_seconds(&self) pub fn rhi::RhiAdmittedTradeMutationEvent::original_bytes(&self) -> &[u8] impl core::fmt::Debug for rhi::RhiAdmittedTradeMutationEvent pub fn rhi::RhiAdmittedTradeMutationEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiBoundCommonAdminServer +impl rhi::RhiBoundCommonAdminServer +pub async fn rhi::RhiBoundCommonAdminServer::serve(self, rhi::RhiAdminCancellationToken) -> core::result::Result<(), rhi::RhiAdminServerError> +impl core::fmt::Debug for rhi::RhiBoundCommonAdminServer +pub fn rhi::RhiBoundCommonAdminServer::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 @@ -638,6 +752,15 @@ pub const fn rhi::RhiCommittedPublication::exact_signed_event_bytes(&self) -> &[ pub const fn rhi::RhiCommittedPublication::outbox_id(&self) -> rhi::RhiPublicationOutboxId impl core::fmt::Debug for rhi::RhiCommittedPublication pub fn rhi::RhiCommittedPublication::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiCommonAdminRouter +impl core::fmt::Debug for rhi::RhiCommonAdminRouter +pub fn rhi::RhiCommonAdminRouter::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiCommonAdminServer +impl rhi::RhiCommonAdminServer +pub async fn rhi::RhiCommonAdminServer::bind(self, &rhi::RhiRuntimeContext) -> core::result::Result<rhi::RhiBoundCommonAdminServer, rhi::RhiAdminServerError> +pub fn rhi::RhiCommonAdminServer::new<H>(&rhi::RhiConfigDocumentV1, alloc::sync::Arc<H>) -> core::result::Result<Self, rhi::RhiAdminServerError> where H: rhi::RhiAdminHandler +impl core::fmt::Debug for rhi::RhiCommonAdminServer +pub fn rhi::RhiCommonAdminServer::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct rhi::RhiConfigApplyError impl rhi::RhiConfigApplyError pub const fn rhi::RhiConfigApplyError::code(self) -> &'static str @@ -1869,6 +1992,8 @@ pub const rhi::RHI_TRADE_SOURCE_RESULT_MAX_BYTES: usize pub const rhi::RHI_TRADE_SOURCE_RESULT_MAX_EVENTS: usize pub const rhi::RHI_WRAPPING_CREDENTIAL_ARTIFACT_BYTES: usize pub const rhi::RHI_WRAPPING_CREDENTIAL_CONTRACT_VERSION: u32 +pub trait rhi::RhiAdminHandler: core::marker::Send + core::marker::Sync + 'static +pub fn rhi::RhiAdminHandler::handle<'a>(&'a self, rhi::RhiAdminRequestDocument) -> rhi::RhiAdminFuture<'a> pub trait rhi::RhiCredentialAccess: core::marker::Send + core::marker::Sync pub fn rhi::RhiCredentialAccess::resolve_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError> impl rhi::RhiCredentialAccess for rhi::CanonicalRhiCredentialAccess @@ -1884,6 +2009,7 @@ pub fn rhi::CanonicalRhiIdentityAccess::open_existing(&self, &rhi::RhiIdentityEn pub fn rhi::admit_rhi_trade_mutation_event(rhi::RhiTradeMutationAdmissionLimits, &[u8], rhi::RhiTradeMutationObservedAtUnixSeconds, rhi::RhiTradeMutationAuthoredTimePolicy) -> core::result::Result<rhi::RhiAdmittedTradeMutationEvent, rhi::RhiTradeMutationAdmissionError> pub async fn rhi::apply_rhi_configuration(&rhi::RhiRuntimeContext, &rhi::RhiConfigDocumentV1, &rhi::RhiConfigDocumentV1, radroots_service_sqlite::migration::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<rhi::RhiConfigApplyOutcome, rhi::RhiConfigApplyError> pub fn rhi::attest_projection_claim(&radroots_trade::trade_contract_v1::RadrootsTradeProjectionV1, &radroots_event::id::MutationId, &rhi::TradeAgreementAttestationPolicy) -> core::result::Result<rhi::TradeAgreementAttestationReportV1, rhi::TradeAgreementAttestationError> +pub fn rhi::build_rhi_common_admin_router<H>(alloc::sync::Arc<H>) -> core::result::Result<rhi::RhiCommonAdminRouter, rhi::RhiAdminRouterError> where H: rhi::RhiAdminHandler pub fn rhi::build_rhi_signed_evidence_attestation(rhi::RhiReconciliationFinalizationFence, &rhi::RhiDecryptedIdentity, radroots_service_host::time::UnixTimeSeconds, &dyn radroots_service_host::entropy::EntropySource, core::option::Option<rhi::RhiEvidenceAttestationSupersession>) -> core::result::Result<rhi::RhiSignedEvidenceAttestation, rhi::RhiReconciliationAttestationError> pub fn rhi::build_rhi_signed_presence_documents(rhi::RhiPresenceDesiredCommitOutcome, &rhi::RhiPresenceDesiredAuthority, &rhi::RhiDecryptedIdentity, radroots_service_host::time::UnixTimeSeconds, &dyn radroots_service_host::entropy::EntropySource) -> core::result::Result<rhi::RhiSignedPresenceDocuments, rhi::RhiPresencePublicationError> pub fn rhi::evaluate_rhi_reconciliation_claim(rhi::RhiReconciliationProjection, radroots_event::id::MutationId) -> rhi::RhiReconciliationEvaluation @@ -1910,5 +2036,6 @@ pub fn rhi::validate_rhi_presence_desired_authority(&rhi::RhiConfigDocumentV1, & pub fn rhi::validate_rhi_signed_presence_documents(&rhi::RhiSignedPresenceDocuments, &rhi::RhiPresenceDesiredAuthority) -> core::result::Result<(), rhi::RhiPresencePublicationError> pub fn rhi::validate_rhi_state_catalogs(&radroots_service_sqlite::migration::MigrationCatalog, &radroots_service_sqlite::integrity::catalog::SchemaCatalog) -> core::result::Result<(), rhi::RhiStateCatalogError> pub fn rhi::verify_rhi_state_backup(&[u8], radroots_service_sqlite::backup::manifest::BackupManifestSha256, &std::path::Path, &rhi::RhiStateMetadata, core::num::nonzero::NonZeroU64) -> core::result::Result<rhi::RhiVerifiedStateBackup, rhi::RhiStateMaintenanceError> +pub type rhi::RhiAdminFuture<'a> = core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<rhi::RhiAdminResponseDocument, rhi::RhiAdminHandlerError>> + core::marker::Send + 'a)>> pub type rhi::RhiReconciliationCoverage = radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1 pub type rhi::RhiReconciliationOutcome = radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1 diff --git a/contracts/services_hardening/admin_common.v1.json b/contracts/services_hardening/admin_common.v1.json @@ -0,0 +1,54 @@ +{ + "schema": "radroots.rhi.admin-common.v1", + "contract_version": 1, + "service": "rhi", + "shared_transport": "radroots_service_host", + "final_inventory": { + "route_count": 22, + "model_count": 36, + "source": "operator_contract.v1.json" + }, + "registered_routes": [ + "radroots.rhi.status.get.v1", + "radroots.rhi.config.effective.get.v1", + "radroots.rhi.identity.status.get.v1", + "radroots.rhi.identity.public.get.v1", + "radroots.rhi.state.status.get.v1", + "radroots.rhi.state.backup.create.v1", + "radroots.rhi.metrics.snapshot.get.v1" + ], + "deferred_route_groups": [ + "domain_queries_and_mutations_step_207", + "identity_and_sensitive_mutations_step_208" + ], + "authority": { + "raw_shared_router_public": false, + "raw_listener_public": false, + "raw_json_handler_public": false, + "filesystem_socket_path_caller_selected": false, + "runtime_context_required": true, + "normalized_transport_limits_required": true, + "system_entropy_owned_by_server": true, + "mutation_success_after_authoritative_commit": true, + "relay_delivery_required_for_success": false + }, + "wire": { + "http_version": "1.1", + "transport": "unix_domain_socket", + "unknown_fields": "reject", + "duplicate_fields": "reject", + "null": "forbidden", + "canonical_response_json": true, + "response_body_max_utf8_bytes": 1048576 + }, + "effects": { + "server_construction_reads_clock": false, + "server_construction_reads_entropy": false, + "router_construction_performs_io": false, + "bind_spawns_task": false, + "serve_owns_process_or_signal_policy": false, + "tcp_admin": false, + "nix": false, + "oci": false + } +} diff --git a/src/admin_v1.rs b/src/admin_v1.rs @@ -0,0 +1,2135 @@ +//! Exact RHI v1 Unix-admin route and model boundary. + +use core::{fmt, future::Future, pin::Pin, time::Duration}; +use std::{ + collections::BTreeSet, + error::Error, + sync::{Arc, OnceLock}, +}; + +use radroots_service_host::{ + AdminCorrelationId, AdminError, AdminErrorCode, AdminErrorMessage, AdminHttpMethod, + AdminMutationRequest, AdminOperationId, AdminRequest, AdminRouteFailure, + AdminRouteFailureStatus, AdminRouteOutcome, AdminRouter, AdminServer, AdminServerError, + AdminTransportLimitValues, AdminTransportLimits, CancellationToken, UnixAdminSocketBinding, + UnixAdminSocketWriterAuthority, +}; +use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; +use serde_json::{Map, Value}; + +// Original model admission is never permitted to exceed the complete response +// body cap enforced again by the shared host after envelope encoding. +const RHI_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES: usize = 1_048_576; + +const OPERATOR_CONTRACT: &str = + include_str!("../contracts/services_hardening/operator_contract.v1.json"); + +/// Closed RHI v1 admin method vocabulary. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RhiAdminMethod { + Get, + Post, +} + +/// Closed RHI v1 Unix-admin route inventory. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RhiAdminRoute { + Status, + EffectiveConfig, + IdentityStatus, + IdentityRekey, + IdentityReplace, + IdentityPublic, + StateStatus, + StateBackup, + MetricsSnapshot, + ReconciliationStatus, + ReconciliationJobs, + ReconciliationRefresh, + Sources, + TradeProjection, + TradeReportCurrent, + TradeReports, + PublicationBacklog, + PublicationTargets, + PublicationRetry, + PresenceDesired, + PresenceRender, + PresenceRefresh, +} + +impl RhiAdminRoute { + /// Complete final machine-governed route inventory. + pub const ALL: [Self; 22] = [ + Self::Status, + Self::EffectiveConfig, + Self::IdentityStatus, + Self::IdentityRekey, + Self::IdentityReplace, + Self::IdentityPublic, + Self::StateStatus, + Self::StateBackup, + Self::MetricsSnapshot, + Self::ReconciliationStatus, + Self::ReconciliationJobs, + Self::ReconciliationRefresh, + Self::Sources, + Self::TradeProjection, + Self::TradeReportCurrent, + Self::TradeReports, + Self::PublicationBacklog, + Self::PublicationTargets, + Self::PublicationRetry, + Self::PresenceDesired, + Self::PresenceRender, + Self::PresenceRefresh, + ]; + + /// Common routes admitted by the Step 206 partial control surface. + pub const COMMON: [Self; 7] = [ + Self::Status, + Self::EffectiveConfig, + Self::IdentityStatus, + Self::IdentityPublic, + Self::StateStatus, + Self::StateBackup, + Self::MetricsSnapshot, + ]; + + #[must_use] + pub const fn method(self) -> RhiAdminMethod { + match self { + Self::Status + | Self::EffectiveConfig + | Self::IdentityStatus + | Self::IdentityPublic + | Self::StateStatus + | Self::MetricsSnapshot + | Self::ReconciliationStatus + | Self::ReconciliationJobs + | Self::Sources + | Self::TradeProjection + | Self::TradeReportCurrent + | Self::TradeReports + | Self::PublicationBacklog + | Self::PublicationTargets + | Self::PresenceDesired => RhiAdminMethod::Get, + Self::StateBackup + | Self::IdentityRekey + | Self::IdentityReplace + | Self::ReconciliationRefresh + | Self::PublicationRetry + | Self::PresenceRender + | Self::PresenceRefresh => RhiAdminMethod::Post, + } + } + + #[must_use] + pub const fn path(self) -> &'static str { + match self { + Self::Status => "/v1/status", + Self::EffectiveConfig => "/v1/config/effective", + Self::IdentityStatus => "/v1/identity/status", + Self::IdentityRekey => "/v1/identity/rekey", + Self::IdentityReplace => "/v1/identity/replace", + Self::IdentityPublic => "/v1/identity/public", + Self::StateStatus => "/v1/state/status", + Self::StateBackup => "/v1/state/backup", + Self::MetricsSnapshot => "/v1/metrics/snapshot", + Self::ReconciliationStatus => "/v1/reconciliation/status", + Self::ReconciliationJobs => "/v1/reconciliation/jobs", + Self::ReconciliationRefresh => "/v1/reconciliation/refresh", + Self::Sources => "/v1/sources", + Self::TradeProjection => "/v1/trades/{trade_id}/projection", + Self::TradeReportCurrent => "/v1/trades/{trade_id}/reports/current", + Self::TradeReports => "/v1/trades/{trade_id}/reports", + Self::PublicationBacklog => "/v1/publication/backlog", + Self::PublicationTargets => "/v1/publication/targets", + Self::PublicationRetry => "/v1/publication/retry", + Self::PresenceDesired => "/v1/presence/desired", + Self::PresenceRender => "/v1/presence/render", + Self::PresenceRefresh => "/v1/presence/refresh", + } + } + + #[must_use] + pub const fn operation_id(self) -> &'static str { + match self { + Self::Status => "radroots.rhi.status.get.v1", + Self::EffectiveConfig => "radroots.rhi.config.effective.get.v1", + Self::IdentityStatus => "radroots.rhi.identity.status.get.v1", + Self::IdentityRekey => "radroots.rhi.identity.rekey.v1", + Self::IdentityReplace => "radroots.rhi.identity.replace.v1", + Self::IdentityPublic => "radroots.rhi.identity.public.get.v1", + Self::StateStatus => "radroots.rhi.state.status.get.v1", + Self::StateBackup => "radroots.rhi.state.backup.create.v1", + Self::MetricsSnapshot => "radroots.rhi.metrics.snapshot.get.v1", + Self::ReconciliationStatus => "radroots.rhi.reconciliation.status.get.v1", + Self::ReconciliationJobs => "radroots.rhi.reconciliation.jobs.list.v1", + Self::ReconciliationRefresh => "radroots.rhi.reconciliation.refresh.v1", + Self::Sources => "radroots.rhi.sources.list.v1", + Self::TradeProjection => "radroots.rhi.trade.projection.get.v1", + Self::TradeReportCurrent => "radroots.rhi.trade.report.current.get.v1", + Self::TradeReports => "radroots.rhi.trade.reports.list.v1", + Self::PublicationBacklog => "radroots.rhi.publication.backlog.list.v1", + Self::PublicationTargets => "radroots.rhi.publication.targets.list.v1", + Self::PublicationRetry => "radroots.rhi.publication.retry.v1", + Self::PresenceDesired => "radroots.rhi.presence.desired.get.v1", + Self::PresenceRender => "radroots.rhi.presence.render.v1", + Self::PresenceRefresh => "radroots.rhi.presence.refresh.v1", + } + } + + #[must_use] + pub const fn request_model(self) -> &'static str { + match self { + Self::Status + | Self::EffectiveConfig + | Self::StateStatus + | Self::MetricsSnapshot + | Self::ReconciliationStatus + | Self::TradeProjection + | Self::TradeReportCurrent + | Self::PresenceDesired => "empty", + Self::IdentityStatus => "identity_status_query_v1", + Self::IdentityRekey => "identity_rekey_request_v1", + Self::IdentityReplace => "identity_replace_request_v1", + Self::IdentityPublic => "identity_public_query_v1", + Self::StateBackup => "state_backup_request_v1", + Self::ReconciliationJobs => "reconciliation_jobs_query_v1", + Self::ReconciliationRefresh => "reconciliation_refresh_request_v1", + Self::Sources => "sources_query_v1", + Self::TradeReports => "reports_query_v1", + Self::PublicationBacklog => "publication_backlog_query_v1", + Self::PublicationTargets => "publication_targets_query_v1", + Self::PublicationRetry => "publication_retry_request_v1", + Self::PresenceRender => "presence_render_request_v1", + Self::PresenceRefresh => "presence_refresh_request_v1", + } + } + + #[must_use] + pub const fn response_model(self) -> &'static str { + match self { + Self::Status => "service_status_v1", + Self::EffectiveConfig => "effective_config_v1", + Self::IdentityStatus => "identity_status_v1", + Self::IdentityRekey | Self::IdentityReplace => "identity_mutation_receipt_v1", + Self::IdentityPublic => "identity_public_v1", + Self::StateStatus => "state_status_v1", + Self::StateBackup => "state_backup_receipt_v1", + Self::MetricsSnapshot => "metrics_snapshot_v1", + Self::ReconciliationStatus => "reconciliation_status_v1", + Self::ReconciliationJobs => "reconciliation_jobs_page_v1", + Self::ReconciliationRefresh => "reconciliation_refresh_receipt_v1", + Self::Sources => "sources_page_v1", + Self::TradeProjection => "trade_projection_v1", + Self::TradeReportCurrent => "report_detail_v1", + Self::TradeReports => "reports_page_v1", + Self::PublicationBacklog => "publication_backlog_page_v1", + Self::PublicationTargets => "publication_targets_page_v1", + Self::PublicationRetry => "publication_retry_receipt_v1", + Self::PresenceDesired => "presence_desired_v1", + Self::PresenceRender => "presence_render_receipt_v1", + Self::PresenceRefresh => "presence_refresh_receipt_v1", + } + } + + #[must_use] + pub const fn is_mutation(self) -> bool { + matches!(self.method(), RhiAdminMethod::Post) + } + + const fn host_method(self) -> AdminHttpMethod { + match self.method() { + RhiAdminMethod::Get => AdminHttpMethod::Get, + RhiAdminMethod::Post => AdminHttpMethod::Post, + } + } + + const fn parameter_binding(self) -> Option<(&'static str, &'static str)> { + match self { + Self::TradeProjection | Self::TradeReportCurrent | Self::TradeReports => { + Some(("trade_id", "trade_id")) + } + _ => None, + } + } +} + +/// One route-bound, already validated RHI admin request. +/// +/// Construction is sealed to an admitted request from the shared server: +/// +/// ```compile_fail +/// use rhi::{RhiAdminRequestDocument, RhiAdminRoute}; +/// +/// let _ = RhiAdminRequestDocument { +/// route: RhiAdminRoute::Status, +/// operation_id: None, +/// correlation_id: todo!(), +/// parameter: None, +/// model_bytes: Box::new([]), +/// }; +/// ``` +pub struct RhiAdminRequestDocument { + route: RhiAdminRoute, + operation_id: Option<AdminOperationId>, + correlation_id: AdminCorrelationId, + parameter: Option<(&'static str, Box<str>)>, + model_bytes: Box<[u8]>, +} + +impl RhiAdminRequestDocument { + #[must_use] + pub const fn route(&self) -> RhiAdminRoute { + self.route + } + + /// Returns the caller's durable idempotency identity for a mutation. + #[must_use] + pub fn operation_id(&self) -> Option<&str> { + self.operation_id.as_ref().map(AdminOperationId::as_str) + } + + #[must_use] + pub fn correlation_id(&self) -> &str { + self.correlation_id.as_str() + } + + /// Returns a validated percent-decoded path parameter when this route has one. + #[must_use] + pub fn parameter(&self, name: &str) -> Option<&str> { + self.parameter + .as_ref() + .filter(|(parameter_name, _)| *parameter_name == name) + .map(|(_, value)| value.as_ref()) + } + + /// Returns compact canonical JSON for the route's exact request model. + #[must_use] + pub fn model_bytes(&self) -> &[u8] { + &self.model_bytes + } +} + +impl fmt::Debug for RhiAdminRequestDocument { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiAdminRequestDocument") + .field("route", &self.route) + .field("mutation", &self.operation_id.is_some()) + .field("has_parameter", &self.parameter.is_some()) + .field("model", &"[redacted]") + .finish() + } +} + +/// One exact validated response model for a fixed route. +pub struct RhiAdminResponseDocument { + route: RhiAdminRoute, + canonical_bytes: Box<[u8]>, + value: Value, +} + +impl RhiAdminResponseDocument { + /// Admits only compact canonical JSON matching the route's response model. + pub fn from_canonical_bytes( + route: RhiAdminRoute, + bytes: &[u8], + ) -> Result<Self, RhiAdminDocumentError> { + if bytes.is_empty() { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::Malformed, + )); + } + if bytes.len() > RHI_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::TooLarge, + )); + } + let value = strict_json(bytes)?; + validate_model(route.response_model(), &value)?; + let canonical = serde_json::to_vec(&value) + .map_err(|_| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::Malformed))?; + if canonical.as_slice() != bytes { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::NonCanonical, + )); + } + Ok(Self { + route, + canonical_bytes: canonical.into_boxed_slice(), + value, + }) + } + + #[must_use] + pub const fn route(&self) -> RhiAdminRoute { + self.route + } + + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } +} + +impl fmt::Debug for RhiAdminResponseDocument { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiAdminResponseDocument") + .field("route", &self.route) + .field("model", &"[redacted]") + .finish() + } +} + +/// Stable classification for a rejected RHI admin document. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiAdminDocumentErrorKind { + TooLarge, + Malformed, + DuplicateField, + NullForbidden, + NonCanonical, + InvalidModel, +} + +/// Source-free and content-free RHI admin document error. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RhiAdminDocumentError { + kind: RhiAdminDocumentErrorKind, +} + +impl RhiAdminDocumentError { + const fn new(kind: RhiAdminDocumentErrorKind) -> Self { + Self { kind } + } + + #[must_use] + pub const fn kind(self) -> RhiAdminDocumentErrorKind { + self.kind + } +} + +impl fmt::Display for RhiAdminDocumentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RHI admin document is invalid") + } +} + +impl Error for RhiAdminDocumentError {} + +/// Stable route-handler failure mapped to a bounded safe admin response. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiAdminHandlerErrorKind { + InvalidCursor, + OperationIdConflict, + NotFound, + Conflict, + Unavailable, + Internal, +} + +/// Source-free route-handler error. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RhiAdminHandlerError { + kind: RhiAdminHandlerErrorKind, +} + +impl RhiAdminHandlerError { + #[must_use] + pub const fn new(kind: RhiAdminHandlerErrorKind) -> Self { + Self { kind } + } + + #[must_use] + pub const fn kind(self) -> RhiAdminHandlerErrorKind { + self.kind + } +} + +impl fmt::Display for RhiAdminHandlerError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RHI admin operation failed") + } +} + +impl Error for RhiAdminHandlerError {} + +/// Boxed route future used by the sealed RHI admin adapter. +pub type RhiAdminFuture<'a> = Pin< + Box<dyn Future<Output = Result<RhiAdminResponseDocument, RhiAdminHandlerError>> + Send + 'a>, +>; + +/// Domain port behind the exact RHI admin transport. +/// +/// Implementations own authoritative local commit, durable operation-ID +/// replay/conflict handling, and any provider or outbox orchestration. Returning +/// success means that the operation's contract-defined local effect is already +/// committed; relay submission or delivery is not implied. Pagination cursors +/// must be authenticated and bound to the same route, filters, and snapshot; +/// mismatched or invalid cursors return [`RhiAdminHandlerErrorKind::InvalidCursor`]. +/// Reusing an operation ID with identical canonical request bytes returns the +/// original committed response; reuse with different bytes returns +/// [`RhiAdminHandlerErrorKind::OperationIdConflict`]. +pub trait RhiAdminHandler: Send + Sync + 'static { + fn handle<'a>(&'a self, request: RhiAdminRequestDocument) -> RhiAdminFuture<'a>; +} + +/// Source-free router-construction failure. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RhiAdminRouterError; + +impl fmt::Display for RhiAdminRouterError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RHI admin router could not be constructed") + } +} + +impl Error for RhiAdminRouterError {} + +/// Opaque Step 206 common-route RHI v1 router capability. +/// +/// The underlying shared-host router remains an implementation detail. The +/// later runtime-composition checkpoint consumes this capability without +/// exposing raw listener or transport authority. +/// +/// ```compile_fail +/// use rhi::RhiCommonAdminRouter; +/// +/// let _ = RhiCommonAdminRouter { inner: todo!() }; +/// ``` +pub struct RhiCommonAdminRouter { + inner: AdminRouter, +} + +impl fmt::Debug for RhiCommonAdminRouter { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { inner } = self; + let _ = inner; + formatter.write_str("RhiCommonAdminRouter") + } +} + +impl RhiCommonAdminRouter { + fn into_inner(self) -> AdminRouter { + self.inner + } +} + +/// Cloneable cooperative cancellation for the RHI Unix-admin server. +#[derive(Clone, Default)] +pub struct RhiAdminCancellationToken { + inner: CancellationToken, +} + +impl RhiAdminCancellationToken { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Requests cancellation. Repeated requests have no additional effect. + pub fn cancel(&self) { + self.inner.cancel(); + } + + #[must_use] + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } +} + +impl fmt::Debug for RhiAdminCancellationToken { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiAdminCancellationToken") + .field("cancelled", &self.is_cancelled()) + .finish() + } +} + +/// Stable source-free RHI Unix-admin server failure classification. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiAdminServerErrorKind { + InvalidConfiguration, + Router, + ServerConfiguration, + WriterAuthority, + Bind, + Listener, + Accept, + ConnectionTaskPanicked, +} + +impl RhiAdminServerErrorKind { + #[must_use] + pub const fn code(self) -> &'static str { + match self { + Self::InvalidConfiguration => "admin_configuration_invalid", + Self::Router => "admin_router_invalid", + Self::ServerConfiguration => "admin_server_configuration_invalid", + Self::WriterAuthority => "admin_writer_authority_unavailable", + Self::Bind => "admin_bind_failed", + Self::Listener => "admin_listener_failed", + Self::Accept => "admin_accept_failed", + Self::ConnectionTaskPanicked => "admin_connection_task_panicked", + } + } +} + +/// One redacted source-free RHI Unix-admin server failure. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct RhiAdminServerError { + kind: RhiAdminServerErrorKind, +} + +impl RhiAdminServerError { + const fn new(kind: RhiAdminServerErrorKind) -> Self { + Self { kind } + } + + #[must_use] + pub const fn kind(self) -> RhiAdminServerErrorKind { + self.kind + } + + #[must_use] + pub const fn code(self) -> &'static str { + self.kind.code() + } +} + +impl fmt::Debug for RhiAdminServerError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiAdminServerError") + .field("kind", &self.kind) + .finish() + } +} + +impl fmt::Display for RhiAdminServerError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RHI Unix-admin server failed") + } +} + +impl Error for RhiAdminServerError {} + +/// Unbound Step 206 common-route RHI Unix-admin server. +/// +/// Construction projects only the already-admitted Rhi configuration, seals +/// the exact route inventory around the supplied domain handler, and uses the +/// shared host's system entropy. The raw shared router and server never cross +/// this boundary. +pub struct RhiCommonAdminServer { + inner: AdminServer, +} + +impl RhiCommonAdminServer { + pub fn new<H>( + configuration: &crate::RhiConfigDocumentV1, + handler: Arc<H>, + ) -> Result<Self, RhiAdminServerError> + where + H: RhiAdminHandler, + { + let limits = admin_transport_limits(configuration)?; + let router = build_rhi_common_admin_router(handler) + .map_err(|_| RhiAdminServerError::new(RhiAdminServerErrorKind::Router))?; + let inner = AdminServer::with_system_entropy(router.into_inner(), limits) + .map_err(|_| RhiAdminServerError::new(RhiAdminServerErrorKind::ServerConfiguration))?; + Ok(Self { inner }) + } + + /// Acquires the canonical runtime-directory authority and binds `admin.sock`. + /// + /// Binding does not spawn a task or begin request admission. Unit 15 owns + /// the final supervised server task and its shutdown phase. + pub async fn bind( + self, + runtime: &crate::RhiRuntimeContext, + ) -> Result<RhiBoundCommonAdminServer, RhiAdminServerError> { + let authority = UnixAdminSocketWriterAuthority::acquire(runtime.context().paths().run()) + .map_err(|_| RhiAdminServerError::new(RhiAdminServerErrorKind::WriterAuthority))?; + let binding = UnixAdminSocketBinding::bind(authority, runtime.artifacts().admin_socket()) + .await + .map_err(|_| RhiAdminServerError::new(RhiAdminServerErrorKind::Bind))?; + Ok(RhiBoundCommonAdminServer { + inner: self.inner, + binding, + }) + } +} + +impl fmt::Debug for RhiCommonAdminServer { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RhiCommonAdminServer([sealed])") + } +} + +/// Bound Step 206 common-route RHI Unix-admin server. +pub struct RhiBoundCommonAdminServer { + inner: AdminServer, + binding: UnixAdminSocketBinding, +} + +impl RhiBoundCommonAdminServer { + /// Serves until supervisor cancellation and then drains bounded connection work. + pub async fn serve( + self, + cancellation: RhiAdminCancellationToken, + ) -> Result<(), RhiAdminServerError> { + self.inner + .serve(self.binding, cancellation.inner) + .await + .map_err(map_admin_server_error) + } +} + +impl fmt::Debug for RhiBoundCommonAdminServer { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RhiBoundCommonAdminServer([sealed])") + } +} + +/// Registers only the seven common Step 206 routes on the hardened Lib router. +/// +/// Domain and sensitive routes remain unregistered until their ordered owners. +pub fn build_rhi_common_admin_router<H>( + handler: Arc<H>, +) -> Result<RhiCommonAdminRouter, RhiAdminRouterError> +where + H: RhiAdminHandler, +{ + if !operator_route_inventory_is_exact() { + return Err(RhiAdminRouterError); + } + let mut router = AdminRouter::new(); + for route in RhiAdminRoute::COMMON { + let handler = Arc::clone(&handler); + router + .route(route.host_method(), route.path(), move |request| { + let handler = Arc::clone(&handler); + async move { dispatch_route(route, handler, request).await } + }) + .map_err(|_| RhiAdminRouterError)?; + } + Ok(RhiCommonAdminRouter { inner: router }) +} + +pub(crate) fn admin_transport_limits( + configuration: &crate::RhiConfigDocumentV1, +) -> Result<AdminTransportLimits, RhiAdminServerError> { + let admin = configuration + .normalized() + .pointer("/resource_limits/admin") + .ok_or_else(invalid_admin_configuration)?; + let values = AdminTransportLimitValues { + header_count: admin_u32(admin, "/header_count")?, + header_bytes: admin_u32(admin, "/header_bytes")?, + request_body_utf8_bytes: admin_u32(admin, "/request_body_utf8_bytes")?, + response_body_utf8_bytes: admin_u32(admin, "/response_body_utf8_bytes")?, + concurrent_connections: admin_u32(admin, "/concurrent_connections")?, + request_deadline: Duration::from_millis(admin_u64(admin, "/request_deadline_ms")?), + idle_timeout: Duration::from_millis(admin_u64(admin, "/idle_timeout_ms")?), + query_items: admin_u32(admin, "/query_items")?, + }; + AdminTransportLimits::new(values).map_err(|_| invalid_admin_configuration()) +} + +fn admin_u64(value: &Value, pointer: &str) -> Result<u64, RhiAdminServerError> { + value + .pointer(pointer) + .and_then(Value::as_u64) + .ok_or_else(invalid_admin_configuration) +} + +fn admin_u32(value: &Value, pointer: &str) -> Result<u32, RhiAdminServerError> { + u32::try_from(admin_u64(value, pointer)?).map_err(|_| invalid_admin_configuration()) +} + +const fn invalid_admin_configuration() -> RhiAdminServerError { + RhiAdminServerError::new(RhiAdminServerErrorKind::InvalidConfiguration) +} + +const fn map_admin_server_error(error: AdminServerError) -> RhiAdminServerError { + let kind = match error { + AdminServerError::ListenerClone { .. } | AdminServerError::ListenerRegistration { .. } => { + RhiAdminServerErrorKind::Listener + } + AdminServerError::Accept { .. } => RhiAdminServerErrorKind::Accept, + AdminServerError::ConnectionTaskPanicked => RhiAdminServerErrorKind::ConnectionTaskPanicked, + }; + RhiAdminServerError::new(kind) +} + +async fn dispatch_route<H>( + route: RhiAdminRoute, + handler: Arc<H>, + request: AdminRequest, +) -> AdminRouteOutcome +where + H: RhiAdminHandler, +{ + let document = match request_document(route, &request) { + Ok(document) => document, + Err(_) => return failure(RhiAdminHandlerErrorKind::Conflict, true), + }; + match handler.handle(document).await { + Ok(response) if response.route == route => match request.success(&response.value) { + Ok(outcome) => outcome, + Err(_) => failure(RhiAdminHandlerErrorKind::Internal, false), + }, + Ok(_) => failure(RhiAdminHandlerErrorKind::Internal, false), + Err(error) => failure(error.kind, false), + } +} + +fn failure(kind: RhiAdminHandlerErrorKind, invalid_request: bool) -> AdminRouteOutcome { + let (status, code, message) = if invalid_request { + ( + AdminRouteFailureStatus::BadRequest, + "invalid_request", + "admin request does not match the route model", + ) + } else { + match kind { + RhiAdminHandlerErrorKind::InvalidCursor => ( + AdminRouteFailureStatus::BadRequest, + "invalid_cursor", + "admin pagination cursor is invalid", + ), + RhiAdminHandlerErrorKind::OperationIdConflict => ( + AdminRouteFailureStatus::Conflict, + "operation_id_conflict", + "admin operation identity conflicts with retained state", + ), + RhiAdminHandlerErrorKind::NotFound => ( + AdminRouteFailureStatus::NotFound, + "not_found", + "admin resource was not found", + ), + RhiAdminHandlerErrorKind::Conflict => ( + AdminRouteFailureStatus::Conflict, + "operation_conflict", + "admin operation conflicts with current state", + ), + RhiAdminHandlerErrorKind::Unavailable => ( + AdminRouteFailureStatus::Unavailable, + "service_unavailable", + "admin operation is temporarily unavailable", + ), + RhiAdminHandlerErrorKind::Internal => ( + AdminRouteFailureStatus::Internal, + "internal_error", + "admin operation failed internally", + ), + } + }; + let code = AdminErrorCode::new(code).expect("fixed admin error code"); + let message = AdminErrorMessage::new(message).expect("fixed admin error message"); + AdminRouteOutcome::failure(AdminRouteFailure::new( + status, + AdminError::new(code, message), + )) +} + +fn request_document( + route: RhiAdminRoute, + request: &AdminRequest, +) -> Result<RhiAdminRequestDocument, RhiAdminDocumentError> { + let (operation_id, value) = match route.method() { + RhiAdminMethod::Get => (None, query_model(route, request.query())?), + RhiAdminMethod::Post => { + let envelope = request + .decode_json::<AdminMutationRequest<Value>>() + .map_err(|_| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::Malformed))?; + ( + Some(envelope.operation_id().clone()), + envelope.into_request(), + ) + } + }; + validate_model(route.request_model(), &value)?; + let model_bytes = serde_json::to_vec(&value) + .map_err(|_| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::Malformed))?; + let parameter = route + .parameter_binding() + .map(|(name, type_name)| { + let value = request + .parameter(name) + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel))? + .to_owned() + .into_boxed_str(); + validate_type(type_name, &Value::String(value.to_string()), 0)?; + Ok((name, value)) + }) + .transpose()?; + Ok(RhiAdminRequestDocument { + route, + operation_id, + correlation_id: request.correlation_id().clone(), + parameter, + model_bytes: model_bytes.into_boxed_slice(), + }) +} + +fn query_model(route: RhiAdminRoute, query: Option<&str>) -> Result<Value, RhiAdminDocumentError> { + let Some(query) = query else { + return Ok(Value::Object(Map::new())); + }; + if query.is_empty() { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::InvalidModel, + )); + } + let fields = model_fields(route.request_model())?; + let mut output = Map::new(); + for item in query.split('&') { + let (raw_key, raw_value) = item + .split_once('=') + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel))?; + if raw_key.is_empty() + || !valid_percent_encoding(raw_key) + || !valid_percent_encoding(raw_value) + { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::InvalidModel, + )); + } + let key = percent_decode(raw_key)?; + let value = percent_decode(raw_value)?; + if output.contains_key(&key) { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::DuplicateField, + )); + } + let descriptor = fields + .get(&key) + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel))?; + let type_name = descriptor + .get("type") + .and_then(Value::as_str) + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel))?; + output.insert(key, query_scalar(type_name, value)?); + } + Ok(Value::Object(output)) +} + +fn query_scalar(type_name: &str, value: String) -> Result<Value, RhiAdminDocumentError> { + let descriptor = type_descriptor(type_name)?; + match descriptor.get("kind").and_then(Value::as_str) { + Some("integer") => { + let canonical = value == "0" + || (value.as_bytes().first().is_some_and(u8::is_ascii_digit) + && !value.starts_with('0') + && value.bytes().all(|byte| byte.is_ascii_digit())); + if !canonical { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::InvalidModel, + )); + } + value + .parse::<u64>() + .map(Value::from) + .map_err(|_| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel)) + } + Some("boolean") => match value.as_str() { + "true" => Ok(Value::Bool(true)), + "false" => Ok(Value::Bool(false)), + _ => Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::InvalidModel, + )), + }, + _ => Ok(Value::String(value)), + } +} + +fn percent_decode(value: &str) -> Result<String, RhiAdminDocumentError> { + let bytes = value.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'%' => { + let high = hex_nibble(bytes[index + 1]).ok_or_else(invalid_model_error)?; + let low = hex_nibble(bytes[index + 2]).ok_or_else(invalid_model_error)?; + decoded.push((high << 4) | low); + index += 3; + } + b'+' => { + decoded.push(b' '); + index += 1; + } + byte => { + decoded.push(byte); + index += 1; + } + } + } + String::from_utf8(decoded).map_err(|_| invalid_model_error()) +} + +const fn hex_nibble(byte: u8) -> Option<u8> { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} + +fn valid_percent_encoding(value: &str) -> bool { + let bytes = value.as_bytes(); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'%' { + if index + 2 >= bytes.len() + || !bytes[index + 1].is_ascii_hexdigit() + || !bytes[index + 2].is_ascii_hexdigit() + { + return false; + } + index += 3; + } else { + index += 1; + } + } + true +} + +fn operator_contract() -> Result<&'static Value, RhiAdminDocumentError> { + static CONTRACT: OnceLock<Option<Value>> = OnceLock::new(); + CONTRACT + .get_or_init(|| serde_json::from_str(OPERATOR_CONTRACT).ok()) + .as_ref() + .ok_or_else(invalid_model_error) +} + +fn operator_route_inventory_is_exact() -> bool { + let Ok(contract) = operator_contract() else { + return false; + }; + let Some(admin) = contract.get("admin") else { + return false; + }; + let Some(routes) = admin.get("routes").and_then(Value::as_array) else { + return false; + }; + let Some(models) = admin.get("models").and_then(Value::as_object) else { + return false; + }; + routes.len() == RhiAdminRoute::ALL.len() + && models.len() == 36 + && admin + .pointer("/model_wire_contract/response_body_max_utf8_bytes") + .and_then(Value::as_u64) + == Some(RHI_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES as u64) + && routes.iter().zip(RhiAdminRoute::ALL).all(|(wire, route)| { + wire.get("method").and_then(Value::as_str) + == Some(match route.method() { + RhiAdminMethod::Get => "GET", + RhiAdminMethod::Post => "POST", + }) + && wire.get("path").and_then(Value::as_str) == Some(route.path()) + && wire.get("operation_id").and_then(Value::as_str) == Some(route.operation_id()) + && wire.get("request_model").and_then(Value::as_str) == Some(route.request_model()) + && wire.get("response_model").and_then(Value::as_str) + == Some(route.response_model()) + && wire.get("mutation").and_then(Value::as_bool) == Some(route.is_mutation()) + }) +} + +fn model_fields(model_name: &str) -> Result<&'static Map<String, Value>, RhiAdminDocumentError> { + operator_contract()? + .pointer(&format!("/admin/models/{model_name}/fields")) + .and_then(Value::as_object) + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel)) +} + +fn type_descriptor(type_name: &str) -> Result<&'static Value, RhiAdminDocumentError> { + operator_contract()? + .pointer(&format!("/admin/types/{type_name}")) + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel)) +} + +fn validate_model(model_name: &str, value: &Value) -> Result<(), RhiAdminDocumentError> { + let object = value + .as_object() + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel))?; + let fields = model_fields(model_name)?; + for key in object.keys() { + if !fields.contains_key(key) { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::InvalidModel, + )); + } + } + for (name, field) in fields { + let required = field.get("presence").and_then(Value::as_str) == Some("required"); + let type_name = field + .get("type") + .and_then(Value::as_str) + .ok_or_else(|| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel))?; + match object.get(name) { + Some(value) => validate_type(type_name, value, 0)?, + None if required => { + return Err(RhiAdminDocumentError::new( + RhiAdminDocumentErrorKind::InvalidModel, + )); + } + None => {} + } + } + Ok(()) +} + +fn validate_type( + type_name: &str, + value: &Value, + depth: usize, +) -> Result<(), RhiAdminDocumentError> { + if depth > 24 || value.is_null() { + return Err(RhiAdminDocumentError::new(if value.is_null() { + RhiAdminDocumentErrorKind::NullForbidden + } else { + RhiAdminDocumentErrorKind::InvalidModel + })); + } + let descriptor = type_descriptor(type_name)?; + match descriptor.get("kind").and_then(Value::as_str) { + Some("literal") => { + if descriptor.get("value") != Some(value) { + return invalid_model(); + } + } + Some("boolean") => { + if !value.is_boolean() { + return invalid_model(); + } + } + Some("integer") => validate_integer(descriptor, value)?, + Some("string") => validate_string(descriptor, value)?, + Some("enum") => { + if !descriptor + .get("values") + .and_then(Value::as_array) + .is_some_and(|values| values.contains(value)) + { + return invalid_model(); + } + } + Some("string_union") => validate_string_union(descriptor, value)?, + Some("array") => validate_array(descriptor, value, depth + 1)?, + Some("canonical_delimited_set") => { + validate_delimited_set(descriptor, value, depth + 1)?; + } + Some("map") => validate_map(descriptor, value, depth + 1)?, + Some("closed_object") => validate_closed_object(descriptor, value, depth + 1)?, + Some("tagged_union") => validate_tagged_union(descriptor, value, depth + 1)?, + Some("alias") => validate_type( + descriptor + .get("target") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?, + value, + depth + 1, + )?, + Some("optional") => validate_type( + descriptor + .get("value") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?, + value, + depth + 1, + )?, + Some("canonical_json_object") => { + if !value.is_object() + || serde_json::to_vec(value).ok().is_none_or(|bytes| { + bytes.len() + > descriptor + .get("maximum_utf8_bytes") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0) + }) + { + return invalid_model(); + } + } + _ => return invalid_model(), + } + Ok(()) +} + +fn validate_integer(descriptor: &Value, value: &Value) -> Result<(), RhiAdminDocumentError> { + let number = value.as_u64().ok_or_else(invalid_model_error)?; + let minimum = descriptor + .get("minimum") + .and_then(Value::as_u64) + .unwrap_or(0); + let maximum = descriptor + .get("maximum") + .and_then(Value::as_u64) + .unwrap_or(u64::MAX); + if !(minimum..=maximum).contains(&number) { + return invalid_model(); + } + Ok(()) +} + +fn validate_string(descriptor: &Value, value: &Value) -> Result<(), RhiAdminDocumentError> { + let string = value.as_str().ok_or_else(invalid_model_error)?; + let exact = descriptor + .get("utf8_bytes") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()); + let minimum = descriptor + .get("minimum_utf8_bytes") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0); + let maximum = descriptor + .get("maximum_utf8_bytes") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(usize::MAX); + if exact.is_some_and(|exact| string.len() != exact) + || !(minimum..=maximum).contains(&string.len()) + || string.chars().any(char::is_control) + { + return invalid_model(); + } + if let Some(pattern) = descriptor.get("pattern").and_then(Value::as_str) { + let valid = match pattern { + "^[A-Za-z0-9][A-Za-z0-9._:-]*$" => bounded_id(string), + "^[a-z][a-z0-9_]*$" => safe_code(string), + "^[a-z][a-z0-9_-]*$" => stable_id(string), + "^[0-9a-f]{32}$" => lower_hex(string, 32), + "^[0-9a-f]{64}$" => lower_hex(string, 64), + "^[0-9a-f]{40}$" => lower_hex(string, 40), + "^/" => string.starts_with('/'), + _ => false, + }; + if !valid { + return invalid_model(); + } + } + if descriptor.get("encoding").and_then(Value::as_str) == Some("canonical_base64url_no_padding") + && !canonical_base64url_no_padding(string) + { + return invalid_model(); + } + if let Some(schemes) = descriptor.get("allowed_schemes").and_then(Value::as_array) { + let parsed = url::Url::parse(string).map_err(|_| invalid_model_error())?; + if !schemes + .iter() + .any(|scheme| scheme.as_str() == Some(parsed.scheme())) + { + return invalid_model(); + } + } + Ok(()) +} + +fn validate_string_union(descriptor: &Value, value: &Value) -> Result<(), RhiAdminDocumentError> { + let string = value.as_str().ok_or_else(invalid_model_error)?; + if descriptor + .get("simple_values") + .and_then(Value::as_array) + .is_some_and(|values| values.iter().any(|value| value.as_str() == Some(string))) + { + return Ok(()); + } + let kind = string + .strip_prefix("sign_event:kind:") + .ok_or_else(invalid_model_error)?; + if kind.is_empty() + || (kind.len() > 1 && kind.starts_with('0')) + || !kind.bytes().all(|byte| byte.is_ascii_digit()) + || kind.parse::<u32>().is_err() + || string.len() + > descriptor + .get("maximum_utf8_bytes") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0) + { + return invalid_model(); + } + Ok(()) +} + +fn validate_array( + descriptor: &Value, + value: &Value, + depth: usize, +) -> Result<(), RhiAdminDocumentError> { + let values = value.as_array().ok_or_else(invalid_model_error)?; + let maximum = descriptor + .get("maximum_items") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0); + if values.len() > maximum { + return invalid_model(); + } + let item_type = descriptor + .get("items") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?; + for item in values { + validate_type(item_type, item, depth)?; + } + if descriptor.get("unique").and_then(Value::as_bool) == Some(true) { + let mut unique = BTreeSet::new(); + for item in values { + let encoded = serde_json::to_vec(item).map_err(|_| invalid_model_error())?; + if !unique.insert(encoded) { + return invalid_model(); + } + } + } + if descriptor.get("canonical_sort").is_some() { + let rendered = values + .iter() + .map(|value| value.as_str().ok_or_else(invalid_model_error)) + .collect::<Result<Vec<_>, _>>()?; + if !rendered.windows(2).all(|pair| pair[0] < pair[1]) { + return invalid_model(); + } + } + Ok(()) +} + +fn validate_delimited_set( + descriptor: &Value, + value: &Value, + depth: usize, +) -> Result<(), RhiAdminDocumentError> { + let rendered = value.as_str().ok_or_else(invalid_model_error)?; + let maximum_bytes = descriptor + .get("maximum_utf8_bytes") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0); + if rendered.len() > maximum_bytes { + return invalid_model(); + } + if rendered.is_empty() { + return if descriptor.get("empty_allowed").and_then(Value::as_bool) == Some(true) { + Ok(()) + } else { + invalid_model() + }; + } + let delimiter = descriptor + .get("delimiter") + .and_then(Value::as_str) + .filter(|delimiter| delimiter.len() == 1) + .ok_or_else(invalid_model_error)?; + let items = rendered.split(delimiter).collect::<Vec<_>>(); + let maximum_items = descriptor + .get("maximum_items") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0); + if items.is_empty() + || items.len() > maximum_items + || items.iter().any(|item| item.is_empty()) + || !items.windows(2).all(|pair| pair[0] < pair[1]) + { + return invalid_model(); + } + let item_type = descriptor + .get("items") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?; + for item in items { + validate_type(item_type, &Value::String(item.to_owned()), depth)?; + } + Ok(()) +} + +fn validate_map( + descriptor: &Value, + value: &Value, + depth: usize, +) -> Result<(), RhiAdminDocumentError> { + let values = value.as_object().ok_or_else(invalid_model_error)?; + let maximum_entries = descriptor + .get("maximum_entries") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(0); + if values.len() > maximum_entries { + return invalid_model(); + } + let key_type = descriptor + .get("key") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?; + let value_type = descriptor + .get("value") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?; + for (key, value) in values { + validate_type(key_type, &Value::String(key.clone()), depth)?; + validate_type(value_type, value, depth)?; + } + Ok(()) +} + +fn validate_closed_object( + descriptor: &Value, + value: &Value, + depth: usize, +) -> Result<(), RhiAdminDocumentError> { + let values = value.as_object().ok_or_else(invalid_model_error)?; + let fields = descriptor + .get("fields") + .and_then(Value::as_object) + .ok_or_else(invalid_model_error)?; + if values.keys().any(|key| !fields.contains_key(key)) { + return invalid_model(); + } + for (field, type_name) in fields { + let type_name = type_name.as_str().ok_or_else(invalid_model_error)?; + match values.get(field) { + Some(value) => validate_type(type_name, value, depth)?, + None if type_descriptor(type_name)? + .get("kind") + .and_then(Value::as_str) + == Some("optional") => {} + None => return invalid_model(), + } + } + Ok(()) +} + +fn validate_tagged_union( + descriptor: &Value, + value: &Value, + depth: usize, +) -> Result<(), RhiAdminDocumentError> { + let object = value.as_object().ok_or_else(invalid_model_error)?; + let discriminator = descriptor + .get("discriminator") + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?; + let selected = object.get(discriminator).ok_or_else(invalid_model_error)?; + let variants = descriptor + .get("variants") + .and_then(Value::as_array) + .ok_or_else(invalid_model_error)?; + let mut matched = None; + for variant in variants { + let variant = variant.as_str().ok_or_else(invalid_model_error)?; + let fields = type_descriptor(variant)? + .get("fields") + .and_then(Value::as_object) + .ok_or_else(invalid_model_error)?; + let discriminator_type = fields + .get(discriminator) + .and_then(Value::as_str) + .ok_or_else(invalid_model_error)?; + if type_descriptor(discriminator_type)?.get("value") == Some(selected) + && matched.replace(variant).is_some() + { + return invalid_model(); + } + } + validate_type(matched.ok_or_else(invalid_model_error)?, value, depth) +} + +fn bounded_id(value: &str) -> bool { + (1..=128).contains(&value.len()) + && value + .as_bytes() + .first() + .is_some_and(u8::is_ascii_alphanumeric) + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) +} + +fn safe_code(value: &str) -> bool { + (1..=64).contains(&value.len()) + && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase) + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') +} + +fn stable_id(value: &str) -> bool { + (1..=64).contains(&value.len()) + && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase) + && value.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-') + }) +} + +fn lower_hex(value: &str, exact: usize) -> bool { + value.len() == exact + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn canonical_base64url_no_padding(value: &str) -> bool { + if value.is_empty() || value.len() % 4 == 1 { + return false; + } + let sextet = |byte: u8| match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'-' => Some(62), + b'_' => Some(63), + _ => None, + }; + let bytes = value.as_bytes(); + if bytes.iter().copied().any(|byte| sextet(byte).is_none()) { + return false; + } + let Some(last) = sextet(bytes[bytes.len() - 1]) else { + return false; + }; + match value.len() % 4 { + 0 => true, + 2 => last & 0x0f == 0, + 3 => last & 0x03 == 0, + _ => false, + } +} + +fn invalid_model<T>() -> Result<T, RhiAdminDocumentError> { + Err(invalid_model_error()) +} + +const fn invalid_model_error() -> RhiAdminDocumentError { + RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel) +} + +const DUPLICATE_MARKER: &str = "rhi_admin_duplicate_field"; +const NULL_MARKER: &str = "rhi_admin_null_forbidden"; + +fn strict_json(bytes: &[u8]) -> Result<Value, RhiAdminDocumentError> { + let mut deserializer = serde_json::Deserializer::from_slice(bytes); + let value = StrictValueSeed + .deserialize(&mut deserializer) + .map_err(|error| { + let message = error.to_string(); + if message.contains(DUPLICATE_MARKER) { + RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::DuplicateField) + } else if message.contains(NULL_MARKER) { + RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::NullForbidden) + } else { + RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::Malformed) + } + })?; + deserializer + .end() + .map_err(|_| RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::Malformed))?; + Ok(value) +} + +struct StrictValueSeed; + +impl<'de> DeserializeSeed<'de> for StrictValueSeed { + type Value = Value; + + fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(StrictValueVisitor) + } +} + +struct StrictValueVisitor; + +impl<'de> Visitor<'de> for StrictValueVisitor { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a non-null JSON value") + } + + fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> { + Ok(Value::Bool(value)) + } + + fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> { + Ok(Value::from(value)) + } + + fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> { + Ok(Value::from(value)) + } + + fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> + where + E: de::Error, + { + serde_json::Number::from_f64(value) + .map(Value::Number) + .ok_or_else(|| E::custom("invalid JSON number")) + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> { + Ok(Value::String(value.to_owned())) + } + + fn visit_string<E>(self, value: String) -> Result<Self::Value, E> { + Ok(Value::String(value)) + } + + fn visit_none<E>(self) -> Result<Self::Value, E> + where + E: de::Error, + { + Err(E::custom(NULL_MARKER)) + } + + fn visit_unit<E>(self) -> Result<Self::Value, E> + where + E: de::Error, + { + Err(E::custom(NULL_MARKER)) + } + + fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error> + where + A: SeqAccess<'de>, + { + let mut values = Vec::new(); + while let Some(value) = sequence.next_element_seed(StrictValueSeed)? { + values.push(value); + } + Ok(Value::Array(values)) + } + + fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'de>, + { + let mut values = Map::new(); + while let Some(key) = object.next_key::<String>()? { + if values.contains_key(&key) { + return Err(de::Error::custom(DUPLICATE_MARKER)); + } + let value = object.next_value_seed(StrictValueSeed)?; + values.insert(key, value); + } + Ok(Value::Object(values)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_model(model_name: &str) -> Value { + let fields = model_fields(model_name).expect("governed model"); + let mut object = Map::new(); + for (name, field) in fields { + if field.get("presence").and_then(Value::as_str) == Some("required") { + let type_name = field + .get("type") + .and_then(Value::as_str) + .expect("field type"); + object.insert(name.clone(), sample_type(type_name).expect("required type")); + } + } + Value::Object(object) + } + + fn sample_type(type_name: &str) -> Option<Value> { + let descriptor = type_descriptor(type_name).expect("governed type"); + match descriptor + .get("kind") + .and_then(Value::as_str) + .expect("type kind") + { + "literal" => Some(descriptor.get("value").expect("literal value").clone()), + "boolean" => Some(Value::Bool(false)), + "integer" => Some(Value::from( + descriptor + .get("minimum") + .and_then(Value::as_u64) + .unwrap_or(0), + )), + "string" => { + let value = if descriptor.get("encoding").is_some() { + "AA".to_owned() + } else if descriptor.get("allowed_schemes").is_some() { + "https://example.test/".to_owned() + } else if descriptor.get("pattern").and_then(Value::as_str) == Some("^/") { + "/var/lib/radroots/rhi".to_owned() + } else if let Some(length) = descriptor.get("utf8_bytes").and_then(Value::as_u64) { + "0".repeat(usize::try_from(length).expect("bounded sample length")) + } else { + "x".to_owned() + }; + Some(Value::String(value)) + } + "enum" => descriptor + .get("values") + .and_then(Value::as_array) + .and_then(|values| values.first()) + .cloned(), + "string_union" => descriptor + .get("simple_values") + .and_then(Value::as_array) + .and_then(|values| values.first()) + .cloned(), + "array" => Some(Value::Array(Vec::new())), + "canonical_delimited_set" => Some(Value::String(String::new())), + "map" | "canonical_json_object" => Some(Value::Object(Map::new())), + "closed_object" => { + let mut object = Map::new(); + for (field, field_type) in descriptor + .get("fields") + .and_then(Value::as_object) + .expect("closed fields") + { + if let Some(value) = sample_type(field_type.as_str().expect("closed type")) { + object.insert(field.clone(), value); + } + } + Some(Value::Object(object)) + } + "tagged_union" => descriptor + .get("variants") + .and_then(Value::as_array) + .and_then(|variants| variants.first()) + .and_then(Value::as_str) + .and_then(sample_type), + "alias" => descriptor + .get("target") + .and_then(Value::as_str) + .and_then(sample_type), + "optional" => None, + kind => panic!("unsupported governed kind {kind}"), + } + } + + fn response_document(route: RhiAdminRoute) -> RhiAdminResponseDocument { + let value = sample_model(route.response_model()); + validate_model(route.response_model(), &value).expect("generated response model"); + let bytes = serde_json::to_vec(&value).expect("canonical response bytes"); + RhiAdminResponseDocument::from_canonical_bytes(route, &bytes) + .expect("admitted response document") + } + + #[test] + fn complete_route_and_model_inventory_matches_the_machine_contract() { + assert!(operator_route_inventory_is_exact()); + assert_eq!(RhiAdminRoute::ALL.len(), 22); + assert_eq!(RhiAdminRoute::COMMON.len(), 7); + let referenced = RhiAdminRoute::ALL + .into_iter() + .flat_map(|route| [route.request_model(), route.response_model()]) + .collect::<BTreeSet<_>>(); + let governed = operator_contract().expect("operator contract")["admin"]["models"] + .as_object() + .expect("model inventory") + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + assert_eq!(referenced, governed); + assert_eq!(governed.len(), 36); + assert!( + RhiAdminRoute::COMMON + .into_iter() + .all(|route| route.parameter_binding().is_none()) + ); + for route in [ + RhiAdminRoute::TradeProjection, + RhiAdminRoute::TradeReportCurrent, + RhiAdminRoute::TradeReports, + ] { + assert_eq!(route.parameter_binding(), Some(("trade_id", "trade_id"))); + } + for model in governed { + let value = sample_model(model); + validate_model(model, &value).expect("minimum exact model"); + } + } + + #[test] + fn strict_response_admission_rejects_duplicates_null_and_noncanonical_bytes() { + let route = RhiAdminRoute::IdentityPublic; + let valid = response_document(route); + assert_eq!(valid.route(), route); + assert!(!valid.canonical_bytes().is_empty()); + + let duplicate = br#"{"generation":0,"generation":1,"public_key":"0000000000000000000000000000000000000000000000000000000000000000","role":"service"}"#; + assert_eq!( + RhiAdminResponseDocument::from_canonical_bytes(route, duplicate) + .expect_err("duplicate key") + .kind(), + RhiAdminDocumentErrorKind::DuplicateField + ); + let nested_null = br#"{"generation":0,"public_key":null,"role":"service"}"#; + assert_eq!( + RhiAdminResponseDocument::from_canonical_bytes(route, nested_null) + .expect_err("nested null") + .kind(), + RhiAdminDocumentErrorKind::NullForbidden + ); + let noncanonical = format!(" {}", String::from_utf8_lossy(valid.canonical_bytes())); + assert_eq!( + RhiAdminResponseDocument::from_canonical_bytes(route, noncanonical.as_bytes()) + .expect_err("whitespace") + .kind(), + RhiAdminDocumentErrorKind::NonCanonical + ); + let invalid = br#"{"generation":0,"public_key":"0000000000000000000000000000000000000000000000000000000000000000","role":"administrator"}"#; + assert_eq!( + RhiAdminResponseDocument::from_canonical_bytes(route, invalid) + .expect_err("invalid model") + .kind(), + RhiAdminDocumentErrorKind::InvalidModel + ); + assert_eq!( + RhiAdminResponseDocument::from_canonical_bytes(route, b"") + .expect_err("empty response") + .kind(), + RhiAdminDocumentErrorKind::Malformed + ); + let oversized = vec![b' '; RHI_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES + 1]; + assert_eq!( + RhiAdminResponseDocument::from_canonical_bytes(route, &oversized) + .expect_err("oversized response") + .kind(), + RhiAdminDocumentErrorKind::TooLarge + ); + assert_eq!( + format!("{valid:?}"), + "RhiAdminResponseDocument { route: IdentityPublic, model: \"[redacted]\" }" + ); + } + + #[test] + fn query_and_nested_type_admission_is_exact_and_bounded() { + let identity = query_model(RhiAdminRoute::IdentityStatus, Some("role=service")) + .expect("common identity query"); + validate_model("identity_status_query_v1", &identity).expect("identity query model"); + for invalid in [ + "", + "role=service&role=service", + "role=transport", + "unknown=x", + ] { + let result = query_model(RhiAdminRoute::IdentityStatus, Some(invalid)) + .and_then(|value| validate_model("identity_status_query_v1", &value)); + assert!( + result.is_err(), + "common query `{invalid}` unexpectedly passed" + ); + } + + let jobs = query_model( + RhiAdminRoute::ReconciliationJobs, + Some("cursor=AA&limit=200&state=pending"), + ) + .expect("canonical query"); + validate_model("reconciliation_jobs_query_v1", &jobs).expect("query model"); + for invalid in [ + "limit=01", + "limit=201", + "limit=1&limit=2", + "unknown=x", + "cursor=%", + "cursor=A&limit=1", + "cursor=AB&limit=1", + "cursor=A%3D%3D&limit=1", + "state=unknown", + ] { + let result = query_model(RhiAdminRoute::ReconciliationJobs, Some(invalid)) + .and_then(|value| validate_model("reconciliation_jobs_query_v1", &value)); + assert!(result.is_err(), "query `{invalid}` unexpectedly passed"); + } + validate_type("stable_id", &Value::String("source-1".to_owned()), 0) + .expect("stable identifier"); + assert!(validate_type("stable_id", &Value::String("Source".to_owned()), 0).is_err()); + validate_type("trade_id", &Value::String("0".repeat(32)), 0).expect("trade identifier"); + assert!(validate_type("trade_id", &Value::String("0".repeat(31)), 0).is_err()); + validate_type( + "absolute_path", + &Value::String("/var/lib/radroots/rhi".to_owned()), + 0, + ) + .expect("Unix absolute path"); + assert!( + validate_type( + "absolute_path", + &Value::String("C:\\rhi\\state".to_owned()), + 0, + ) + .is_err() + ); + assert!(validate_type("bounded_id", &Value::String("../escape".to_owned()), 0).is_err()); + } + + #[test] + fn public_diagnostics_are_source_free_and_content_free() { + let document = RhiAdminDocumentError::new(RhiAdminDocumentErrorKind::InvalidModel); + let handler = RhiAdminHandlerError::new(RhiAdminHandlerErrorKind::Internal); + let server = RhiAdminServerError::new(RhiAdminServerErrorKind::Bind); + for rendered in [ + format!("{document}"), + format!("{document:?}"), + format!("{handler}"), + format!("{handler:?}"), + format!("{server}"), + format!("{server:?}"), + ] { + assert!(!rendered.contains("/tmp/protected")); + assert!(!rendered.contains("credential")); + } + assert!(Error::source(&document).is_none()); + assert!(Error::source(&handler).is_none()); + assert!(Error::source(&server).is_none()); + assert_eq!(server.code(), "admin_bind_failed"); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + mod native { + use std::fs; + use std::path::Path; + use std::sync::Mutex; + + use radroots_service_host::{AdminClient, AdminClientTarget, AdminTransportLimits}; + + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); + + type FixtureCall = (RhiAdminRoute, Option<String>, Box<[u8]>); + + struct FixtureHandler { + calls: Mutex<Vec<FixtureCall>>, + } + + impl FixtureHandler { + fn new() -> Self { + Self { + calls: Mutex::new(Vec::new()), + } + } + } + + impl RhiAdminHandler for FixtureHandler { + fn handle<'a>(&'a self, request: RhiAdminRequestDocument) -> RhiAdminFuture<'a> { + Box::pin(async move { + if request.operation_id() == Some("conflict") { + return Err(RhiAdminHandlerError::new( + RhiAdminHandlerErrorKind::OperationIdConflict, + )); + } + self.calls.lock().expect("calls").push(( + request.route(), + request.operation_id().map(str::to_owned), + request.model_bytes().into(), + )); + Ok(response_document(request.route())) + }) + } + } + + fn runtime_context() -> ( + tempfile::TempDir, + crate::RhiRuntimeContext, + crate::RhiConfigDocumentV1, + ) { + let root = tempfile::Builder::new() + .prefix("rhi-admin-") + .tempdir_in("/tmp") + .expect("short runtime root"); + let root_path = root.path().to_str().expect("UTF-8 test root"); + let invocation = crate::parse_rhi_cli_v1_from([ + "rhi", + "--profile", + "repo-local", + "--instance", + "primary", + "--repo-local-root", + root_path, + "run", + ]) + .expect("test CLI"); + let resolver = crate::RadrootsPathResolver::new( + crate::RadrootsPlatform::Linux, + crate::RadrootsHostEnvironment::default(), + ); + let runtime = crate::resolve_rhi_runtime_context(&resolver, &invocation) + .expect("test runtime context"); + fs::create_dir_all(runtime.context().paths().run()).expect("runtime directory"); + let configuration = + crate::parse_rhi_config_v1(CONFIG.as_bytes(), crate::RhiConfigProfile::RepoLocal) + .expect("test configuration"); + (root, runtime, configuration) + } + + fn target_for(route: RhiAdminRoute, request: &Value) -> AdminClientTarget { + let path = route + .path() + .replace("{trade_id}", "00000000000000000000000000000000"); + if !matches!(route.method(), RhiAdminMethod::Get) + || request.as_object().is_some_and(Map::is_empty) + { + return AdminClientTarget::new(path).expect("route target"); + } + let mut serializer = url::form_urlencoded::Serializer::new(String::new()); + for (name, value) in request.as_object().expect("query object") { + let rendered = value + .as_str() + .map(str::to_owned) + .unwrap_or_else(|| value.to_string()); + serializer.append_pair(name, &rendered); + } + AdminClientTarget::new(format!("{path}?{}", serializer.finish())).expect("query target") + } + + async fn raw_post(socket: &Path, body: &str) -> String { + let mut stream = tokio::net::UnixStream::connect(socket) + .await + .expect("raw connection"); + let request = format!( + "POST /v1/state/backup HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream + .write_all(request.as_bytes()) + .await + .expect("raw request"); + let mut response = Vec::new(); + stream + .read_to_end(&mut response) + .await + .expect("raw response"); + String::from_utf8(response).expect("HTTP response") + } + + #[tokio::test] + async fn seven_common_routes_round_trip_over_the_hardened_unix_boundary() { + let (_root, runtime, configuration) = runtime_context(); + let socket = runtime.artifacts().admin_socket().to_path_buf(); + let handler = Arc::new(FixtureHandler::new()); + let server = RhiCommonAdminServer::new(&configuration, Arc::clone(&handler)) + .expect("production admin server") + .bind(&runtime) + .await + .expect("canonical admin binding"); + let cancellation = RhiAdminCancellationToken::new(); + let server_cancellation = cancellation.clone(); + let task = tokio::spawn(async move { + server + .serve(server_cancellation) + .await + .expect("serve RHI admin"); + }); + let client = + AdminClient::new(&socket, AdminTransportLimits::DEFAULT).expect("admin client"); + + for (index, route) in RhiAdminRoute::COMMON.into_iter().enumerate() { + let request = sample_model(route.request_model()); + let target = target_for(route, &request); + let response = match route.method() { + RhiAdminMethod::Get => client.get::<Value>(&target).await.expect("GET route"), + RhiAdminMethod::Post => { + let operation_id = AdminOperationId::new(format!("operation-{index}")) + .expect("operation ID"); + client + .mutate::<_, Value>(&target, operation_id, None, &request) + .await + .expect("POST route") + } + }; + validate_model(route.response_model(), response.result()) + .expect("route response model"); + } + + let conflict_target = + AdminClientTarget::new("/v1/state/backup").expect("mutation target"); + let conflict_error = client + .mutate::<_, Value>( + &conflict_target, + AdminOperationId::new("conflict").expect("operation ID"), + None, + sample_model("state_backup_request_v1"), + ) + .await + .expect_err("operation conflict"); + assert_eq!( + conflict_error + .failure() + .expect("failure envelope") + .error() + .code() + .as_str(), + "operation_id_conflict" + ); + + for body in [ + r#"{"contract_version":1,"operation_id":"duplicate","request":{"confirmation":"confirm","expected_generation":0,"expected_generation":1,"target_path":"/tmp/backup"}}"#, + r#"{"contract_version":1,"operation_id":"null","request":{"confirmation":"confirm","expected_generation":null,"target_path":"/tmp/backup"}}"#, + ] { + let response = raw_post(&socket, body).await; + assert!(response.starts_with("HTTP/1.1 400 "), "{response}"); + } + + let domain_target = + AdminClientTarget::new("/v1/reconciliation/status").expect("deferred domain route"); + assert!(client.get::<Value>(&domain_target).await.is_err()); + + let sensitive_target = + AdminClientTarget::new("/v1/identity/rekey").expect("deferred sensitive route"); + assert!( + client + .mutate::<_, Value>( + &sensitive_target, + AdminOperationId::new("deferred-identity").expect("operation ID"), + None, + sample_model("identity_rekey_request_v1"), + ) + .await + .is_err() + ); + + { + let calls = handler.calls.lock().expect("calls"); + assert_eq!(calls.len(), 7); + for (index, (route, operation_id, request)) in calls.iter().enumerate() { + assert_eq!(*route, RhiAdminRoute::COMMON[index]); + assert_eq!(operation_id.is_some(), route.is_mutation()); + validate_model( + route.request_model(), + &serde_json::from_slice(request).expect("retained request model"), + ) + .expect("retained exact model"); + } + } + cancellation.cancel(); + task.await.expect("server task"); + assert!(!socket.exists()); + } + + #[test] + fn production_server_projects_exact_validated_admin_limits() { + let (_root, _runtime, configuration) = runtime_context(); + assert_eq!( + admin_transport_limits(&configuration).expect("admin limits"), + AdminTransportLimits::DEFAULT + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -3,6 +3,7 @@ #![doc = include_str!("../README")] mod adapters; +mod admin_v1; mod cli_v1; mod config_v1; mod features; @@ -37,6 +38,13 @@ mod state_trade; mod trade_ingest; pub use adapters::nostr::event::NostrEventAdapter; +pub use admin_v1::{ + RhiAdminCancellationToken, RhiAdminDocumentError, RhiAdminDocumentErrorKind, RhiAdminFuture, + RhiAdminHandler, RhiAdminHandlerError, RhiAdminHandlerErrorKind, RhiAdminMethod, + RhiAdminRequestDocument, RhiAdminResponseDocument, RhiAdminRoute, RhiAdminRouterError, + RhiAdminServerError, RhiAdminServerErrorKind, RhiBoundCommonAdminServer, RhiCommonAdminRouter, + RhiCommonAdminServer, build_rhi_common_admin_router, +}; pub use cli_v1::{ RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1, RhiCliV1Error, RhiCliV1ErrorKind, RhiCommandV1, RhiConfigCommandV1, RhiIdentityCommandV1, RhiMetricsCommandV1, diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -4,6 +4,7 @@ const MANIFEST: &str = include_str!("../Cargo.toml"); const README: &str = include_str!("../README"); const AGENTS: &str = include_str!("../AGENTS.md"); const ROOT: &str = include_str!("../src/lib.rs"); +const ADMIN: &str = include_str!("../src/admin_v1.rs"); const ADAPTERS: &str = include_str!("../src/adapters/mod.rs"); const NOSTR_ADAPTERS: &str = include_str!("../src/adapters/nostr/mod.rs"); const FEATURES: &str = include_str!("../src/features/mod.rs"); @@ -70,6 +71,7 @@ const TRADE_SOURCE_INGEST_CONTRACT: &str = const PUBLIC_API: &str = include_str!("../contracts/api_baselines/rhi.txt"); const SOURCES: &[&str] = &[ include_str!("../src/adapters/nostr/event.rs"), + include_str!("../src/admin_v1.rs"), include_str!("../src/cli_v1.rs"), include_str!("../src/config_v1.rs"), include_str!("../src/features/trade_agreement_attestation.rs"), @@ -136,7 +138,7 @@ fn shared_runtime_contracts_are_curated_without_exposing_implementation_authorit "SystemMonotonicClock", "SystemWallClock", "TaskSupervisor", - "CancellationToken", + "pub use radroots_service_host::CancellationToken", ] { assert!( !ROOT.contains(forbidden), @@ -151,6 +153,7 @@ fn shared_runtime_contracts_are_curated_without_exposing_implementation_authorit fn state_catalog_module_is_private_and_root_api_is_curated() { for module in [ "adapters", + "admin_v1", "cli_v1", "config_v1", "features", @@ -227,6 +230,13 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { "build_rhi_signed_presence_documents", "validate_rhi_signed_presence_documents", "RHI_PRESENCE_PUBLICATION_CONTRACT_VERSION", + "RhiAdminRoute", + "RhiAdminRequestDocument", + "RhiAdminResponseDocument", + "RhiAdminHandler", + "RhiCommonAdminServer", + "RhiBoundCommonAdminServer", + "build_rhi_common_admin_router", "RhiPublicationErrorKind", "RhiPublicationMode", "RhiPublicationRetryPolicy", @@ -349,6 +359,37 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { assert!(!PUBLIC_API.contains("rhi::publication_execution::")); assert!(!PUBLIC_API.contains("rhi::publication_submission::")); assert!(!PUBLIC_API.contains("rhi::presence_publication::")); + assert!(!PUBLIC_API.contains("rhi::admin_v1::")); +} + +#[test] +fn common_admin_boundary_hides_shared_transport_authority() { + for required in [ + "pub struct RhiCommonAdminRouter", + "pub struct RhiCommonAdminServer", + "pub struct RhiBoundCommonAdminServer", + "pub trait RhiAdminHandler", + "pub const COMMON: [Self; 7]", + ] { + assert!( + ADMIN.contains(required), + "missing common admin boundary `{required}`" + ); + } + for forbidden in [ + "pub fn into_inner", + "pub fn router", + "pub fn listener", + "pub use radroots_service_host::AdminRouter", + "pub use radroots_service_host::AdminServer", + "pub use serde_json::Value", + ] { + assert!(!ROOT.contains(forbidden), "public root leaks `{forbidden}`"); + assert!( + !PUBLIC_API.contains(forbidden), + "API baseline leaks `{forbidden}`" + ); + } } #[test] @@ -961,7 +1002,7 @@ fn public_errors_are_crate_owned_redacted_and_source_free() { .lines() .filter(|line| line.starts_with("pub struct rhi::") && line.ends_with("Error")) .count(); - assert_eq!(public_error_count, 31); + assert_eq!(public_error_count, 35); } #[test] @@ -1446,6 +1487,11 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() { "[`runtime_foundation.v1.json`](contracts/services_hardening/runtime_foundation.v1.json)", "at most 1,024 consecutive generations", "never stores raw TOML, paths, relay URLs, credential", + "## Common Unix-admin boundary", + "seven common RHI routes", + "Domain and sensitive routes remain deliberately", + "22-route/36-model inventory", + "[`admin_common.v1.json`](contracts/services_hardening/admin_common.v1.json)", ] { assert!(README.contains(required), "README is missing {required}"); } diff --git a/tests/services_hardening_admin_common.rs b/tests/services_hardening_admin_common.rs @@ -0,0 +1,92 @@ +#![forbid(unsafe_code)] + +use std::collections::BTreeSet; + +use serde_json::Value; + +const COMMON_CONTRACT: &str = include_str!("../contracts/services_hardening/admin_common.v1.json"); +const OPERATOR_CONTRACT: &str = + include_str!("../contracts/services_hardening/operator_contract.v1.json"); +const ADMIN_SOURCE: &str = include_str!("../src/admin_v1.rs"); + +#[test] +fn common_route_inventory_is_an_exact_ordered_subset() { + let common: Value = serde_json::from_str(COMMON_CONTRACT).expect("common admin contract"); + let operator: Value = serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + assert_eq!(common["schema"], "radroots.rhi.admin-common.v1"); + assert_eq!(common["contract_version"], 1); + assert_eq!(common["final_inventory"]["route_count"], 22); + assert_eq!(common["final_inventory"]["model_count"], 36); + + let registered = common["registered_routes"] + .as_array() + .expect("registered routes") + .iter() + .map(|value| value.as_str().expect("operation ID")) + .collect::<Vec<_>>(); + assert_eq!( + registered, + [ + "radroots.rhi.status.get.v1", + "radroots.rhi.config.effective.get.v1", + "radroots.rhi.identity.status.get.v1", + "radroots.rhi.identity.public.get.v1", + "radroots.rhi.state.status.get.v1", + "radroots.rhi.state.backup.create.v1", + "radroots.rhi.metrics.snapshot.get.v1", + ] + ); + assert_eq!(registered.iter().copied().collect::<BTreeSet<_>>().len(), 7); + let governed = operator["admin"]["routes"] + .as_array() + .expect("operator routes") + .iter() + .map(|route| route["operation_id"].as_str().expect("operation ID")) + .collect::<BTreeSet<_>>(); + assert!( + registered + .iter() + .all(|operation| governed.contains(operation)) + ); +} + +#[test] +fn common_adapter_is_sealed_bounded_and_partial_by_construction() { + let common: Value = serde_json::from_str(COMMON_CONTRACT).expect("common admin contract"); + assert_eq!(common["shared_transport"], "radroots_service_host"); + assert_eq!(common["authority"]["raw_shared_router_public"], false); + assert_eq!(common["authority"]["raw_listener_public"], false); + assert_eq!(common["authority"]["raw_json_handler_public"], false); + assert_eq!(common["authority"]["runtime_context_required"], true); + assert_eq!(common["wire"]["duplicate_fields"], "reject"); + assert_eq!(common["wire"]["null"], "forbidden"); + assert_eq!(common["wire"]["response_body_max_utf8_bytes"], 1_048_576); + assert_eq!(common["effects"]["router_construction_performs_io"], false); + assert_eq!(common["effects"]["bind_spawns_task"], false); + assert_eq!(common["effects"]["tcp_admin"], false); + + for required in [ + "pub const COMMON: [Self; 7]", + "for route in RhiAdminRoute::COMMON", + "AdminServer::with_system_entropy(router.into_inner(), limits)", + "UnixAdminSocketWriterAuthority::acquire(runtime.context().paths().run())", + "seven_common_routes_round_trip_over_the_hardened_unix_boundary", + ] { + assert!( + ADMIN_SOURCE.contains(required), + "missing boundary `{required}`" + ); + } + for forbidden in [ + "for route in RhiAdminRoute::ALL", + "pub fn into_inner", + "pub fn router", + "TcpListener", + "Cors", + ] { + assert!( + !ADMIN_SOURCE.contains(forbidden), + "forbidden boundary `{forbidden}`" + ); + } +}