rhi

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

commit 18222d3a4cfc2ed21d93db7e2d5d7e5377918aaf
parent fb88b899b93a652e06a27719e51d1761870195de
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 16:24:21 +0000

refactor(rhi): bind domain Unix admin routes

- activate the exact thirteen domain routes through the sealed RHI adapter
- enforce bounded queries, canonical cursors, and typed trade identifiers
- retain handler-owned replay, cursor authentication, and durable commit semantics
- freeze the cumulative contract, tests, documentation, and public API baseline

Diffstat:
MAGENTS.md | 5+++++
MREADME | 22+++++++++++++++++-----
Mcontracts/api_baselines/rhi.txt | 32+++++++++++++++++---------------
Acontracts/services_hardening/admin_domain.v1.json | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/admin_v1.rs | 187++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/lib.rs | 6+++---
Mtests/package_boundary.rs | 24+++++++++++++++---------
Mtests/services_hardening_admin_common.rs | 35++++++++---------------------------
Atests/services_hardening_admin_domain.rs | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 417 insertions(+), 107 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -404,6 +404,11 @@ 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. +- Through Step 207, the active set is exactly seven common plus thirteen + domain routes. Pagination is bounded to 200 items, query fields are closed, + authenticated cursors remain bound by the handler to route/filter/snapshot, + and decoded trade parameters use the exact lowercase-hex trade-ID type. The + two identity-sensitive mutations remain unregistered until Step 208. - 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,9 +550,9 @@ 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 +## Unix-admin boundary -Step 206 binds the seven common RHI routes for detailed status, redacted +Step 206 bound 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 @@ -566,11 +566,23 @@ 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 +the public RHI boundary. Its historical partial machine contract is [`admin_common.v1.json`](contracts/services_hardening/admin_common.v1.json). +Step 207 cumulatively activates the thirteen reconciliation, job, source, +trade projection/report, publication target/backlog, retry, and presence +domain routes. Page sizes stop at 200; query names and duplicates are closed; +cursors use canonical base64url without padding and remain authenticated and +bound by the handler to the same route, filters, and snapshot. Every decoded +`{trade_id}` is exactly 32 lowercase hexadecimal characters. Mutations retain +stable operation-ID exact replay and conflicting-reuse rejection, and success +still means the handler's contract-defined local effect is durably committed, +not relay delivery. The adapter performs no SQLite or relay I/O itself. The +two identity-sensitive mutations remain unregistered for Step 208, before +Step 209 qualifies the complete 22-route/36-model inventory. The cumulative +domain contract is +[`admin_domain.v1.json`](contracts/services_hardening/admin_domain.v1.json). + ## Sealed service-instance paths One validated CLI invocation resolves through `RhiRuntimeContext`, which owns diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt @@ -60,8 +60,10 @@ pub rhi::RhiAdminRoute::TradeProjection pub rhi::RhiAdminRoute::TradeReportCurrent pub rhi::RhiAdminRoute::TradeReports impl rhi::RhiAdminRoute +pub const rhi::RhiAdminRoute::ACTIVE: [Self; 20] pub const rhi::RhiAdminRoute::ALL: [Self; 22] pub const rhi::RhiAdminRoute::COMMON: [Self; 7] +pub const rhi::RhiAdminRoute::DOMAIN: [Self; 13] 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 @@ -697,10 +699,19 @@ pub fn rhi::RhiAdminResponseDocument::from_canonical_bytes(rhi::RhiAdminRoute, & 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::RhiAdminRouter +impl core::fmt::Debug for rhi::RhiAdminRouter +pub fn rhi::RhiAdminRouter::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::RhiAdminServer +impl rhi::RhiAdminServer +pub async fn rhi::RhiAdminServer::bind(self, &rhi::RhiRuntimeContext) -> core::result::Result<rhi::RhiBoundAdminServer, rhi::RhiAdminServerError> +pub fn rhi::RhiAdminServer::new<H>(&rhi::RhiConfigDocumentV1, alloc::sync::Arc<H>) -> core::result::Result<Self, rhi::RhiAdminServerError> where H: rhi::RhiAdminHandler +impl core::fmt::Debug for rhi::RhiAdminServer +pub fn rhi::RhiAdminServer::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 @@ -721,11 +732,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::RhiBoundAdminServer +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::RhiCliInvocationV1 impl rhi::RhiCliInvocationV1 pub const fn rhi::RhiCliInvocationV1::command(&self) -> rhi::RhiCommandV1 @@ -752,15 +763,6 @@ 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 @@ -2009,7 +2011,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_admin_router<H>(alloc::sync::Arc<H>) -> core::result::Result<rhi::RhiAdminRouter, 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 diff --git a/contracts/services_hardening/admin_domain.v1.json b/contracts/services_hardening/admin_domain.v1.json @@ -0,0 +1,63 @@ +{ + "schema": "radroots.rhi.admin-domain.v1", + "contract_version": 1, + "service": "rhi", + "shared_transport": "radroots_service_host", + "final_inventory": { + "route_count": 22, + "model_count": 36, + "source": "operator_contract.v1.json" + }, + "active_route_count": 20, + "newly_registered_routes": [ + "radroots.rhi.reconciliation.status.get.v1", + "radroots.rhi.reconciliation.jobs.list.v1", + "radroots.rhi.reconciliation.refresh.v1", + "radroots.rhi.sources.list.v1", + "radroots.rhi.trade.projection.get.v1", + "radroots.rhi.trade.report.current.get.v1", + "radroots.rhi.trade.reports.list.v1", + "radroots.rhi.publication.backlog.list.v1", + "radroots.rhi.publication.targets.list.v1", + "radroots.rhi.publication.retry.v1", + "radroots.rhi.presence.desired.get.v1", + "radroots.rhi.presence.render.v1", + "radroots.rhi.presence.refresh.v1" + ], + "deferred_routes": [ + "radroots.rhi.identity.rekey.v1", + "radroots.rhi.identity.replace.v1" + ], + "pagination": { + "maximum_page_items": 200, + "cursor_encoding": "canonical_base64url_no_padding", + "cursor_integrity": "server_authenticated", + "cursor_binding": ["route", "filters", "snapshot"], + "duplicate_query_items": "reject", + "unknown_query_items": "reject" + }, + "path_parameters": { + "trade_id": { + "utf8_bytes": 32, + "encoding": "lowercase_hex" + } + }, + "authority": { + "handler_required": true, + "mutation_success_after_authoritative_commit": true, + "operation_id_replay_exact": true, + "operation_id_conflicting_reuse": "reject", + "invalid_cursor_error": "invalid_cursor", + "raw_shared_router_public": false, + "raw_json_handler_public": false + }, + "effects": { + "adapter_performs_sqlite": false, + "adapter_performs_relay_io": false, + "adapter_spawns_tasks": false, + "tcp_admin": false, + "identity_mutation": false, + "nix": false, + "oci": false + } +} diff --git a/src/admin_v1.rs b/src/admin_v1.rs @@ -10,7 +10,8 @@ use std::{ use radroots_service_host::{ AdminCorrelationId, AdminError, AdminErrorCode, AdminErrorMessage, AdminHttpMethod, AdminMutationRequest, AdminOperationId, AdminRequest, AdminRouteFailure, - AdminRouteFailureStatus, AdminRouteOutcome, AdminRouter, AdminServer, AdminServerError, + AdminRouteFailureStatus, AdminRouteOutcome, AdminRouter as SharedAdminRouter, + AdminServer as SharedAdminServer, AdminServerError as SharedAdminServerError, AdminTransportLimitValues, AdminTransportLimits, CancellationToken, UnixAdminSocketBinding, UnixAdminSocketWriterAuthority, }; @@ -96,6 +97,47 @@ impl RhiAdminRoute { Self::MetricsSnapshot, ]; + /// Domain routes admitted by the Step 207 control surface. + pub const DOMAIN: [Self; 13] = [ + 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, + ]; + + /// Routes admitted through Step 207, in final machine-contract order. + pub const ACTIVE: [Self; 20] = [ + Self::Status, + Self::EffectiveConfig, + Self::IdentityStatus, + 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, + ]; + #[must_use] pub const fn method(self) -> RhiAdminMethod { match self { @@ -490,31 +532,31 @@ impl fmt::Display for RhiAdminRouterError { impl Error for RhiAdminRouterError {} -/// Opaque Step 206 common-route RHI v1 router capability. +/// Opaque RHI v1 router capability through Step 207. /// /// 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; +/// use rhi::RhiAdminRouter; /// -/// let _ = RhiCommonAdminRouter { inner: todo!() }; +/// let _ = RhiAdminRouter { inner: todo!() }; /// ``` -pub struct RhiCommonAdminRouter { - inner: AdminRouter, +pub struct RhiAdminRouter { + inner: SharedAdminRouter, } -impl fmt::Debug for RhiCommonAdminRouter { +impl fmt::Debug for RhiAdminRouter { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { inner } = self; let _ = inner; - formatter.write_str("RhiCommonAdminRouter") + formatter.write_str("RhiAdminRouter") } } -impl RhiCommonAdminRouter { - fn into_inner(self) -> AdminRouter { +impl RhiAdminRouter { + fn into_inner(self) -> SharedAdminRouter { self.inner } } @@ -619,17 +661,17 @@ impl fmt::Display for RhiAdminServerError { impl Error for RhiAdminServerError {} -/// Unbound Step 206 common-route RHI Unix-admin server. +/// Unbound RHI Unix-admin server through Step 207. /// /// 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, +pub struct RhiAdminServer { + inner: SharedAdminServer, } -impl RhiCommonAdminServer { +impl RhiAdminServer { pub fn new<H>( configuration: &crate::RhiConfigDocumentV1, handler: Arc<H>, @@ -638,9 +680,9 @@ impl RhiCommonAdminServer { H: RhiAdminHandler, { let limits = admin_transport_limits(configuration)?; - let router = build_rhi_common_admin_router(handler) + let router = build_rhi_admin_router(handler) .map_err(|_| RhiAdminServerError::new(RhiAdminServerErrorKind::Router))?; - let inner = AdminServer::with_system_entropy(router.into_inner(), limits) + let inner = SharedAdminServer::with_system_entropy(router.into_inner(), limits) .map_err(|_| RhiAdminServerError::new(RhiAdminServerErrorKind::ServerConfiguration))?; Ok(Self { inner }) } @@ -652,32 +694,32 @@ impl RhiCommonAdminServer { pub async fn bind( self, runtime: &crate::RhiRuntimeContext, - ) -> Result<RhiBoundCommonAdminServer, RhiAdminServerError> { + ) -> Result<RhiBoundAdminServer, 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 { + Ok(RhiBoundAdminServer { inner: self.inner, binding, }) } } -impl fmt::Debug for RhiCommonAdminServer { +impl fmt::Debug for RhiAdminServer { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("RhiCommonAdminServer([sealed])") + formatter.write_str("RhiAdminServer([sealed])") } } -/// Bound Step 206 common-route RHI Unix-admin server. -pub struct RhiBoundCommonAdminServer { - inner: AdminServer, +/// Bound RHI Unix-admin server through Step 207. +pub struct RhiBoundAdminServer { + inner: SharedAdminServer, binding: UnixAdminSocketBinding, } -impl RhiBoundCommonAdminServer { +impl RhiBoundAdminServer { /// Serves until supervisor cancellation and then drains bounded connection work. pub async fn serve( self, @@ -690,26 +732,24 @@ impl RhiBoundCommonAdminServer { } } -impl fmt::Debug for RhiBoundCommonAdminServer { +impl fmt::Debug for RhiBoundAdminServer { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("RhiBoundCommonAdminServer([sealed])") + formatter.write_str("RhiBoundAdminServer([sealed])") } } -/// Registers only the seven common Step 206 routes on the hardened Lib router. +/// Registers the seven common and thirteen domain routes owned through Step 207. /// -/// Domain and sensitive routes remain unregistered until their ordered owners. -pub fn build_rhi_common_admin_router<H>( - handler: Arc<H>, -) -> Result<RhiCommonAdminRouter, RhiAdminRouterError> +/// The two sensitive identity mutations remain unregistered until Step 208. +pub fn build_rhi_admin_router<H>(handler: Arc<H>) -> Result<RhiAdminRouter, RhiAdminRouterError> where H: RhiAdminHandler, { if !operator_route_inventory_is_exact() { return Err(RhiAdminRouterError); } - let mut router = AdminRouter::new(); - for route in RhiAdminRoute::COMMON { + let mut router = SharedAdminRouter::new(); + for route in RhiAdminRoute::ACTIVE { let handler = Arc::clone(&handler); router .route(route.host_method(), route.path(), move |request| { @@ -718,7 +758,7 @@ where }) .map_err(|_| RhiAdminRouterError)?; } - Ok(RhiCommonAdminRouter { inner: router }) + Ok(RhiAdminRouter { inner: router }) } pub(crate) fn admin_transport_limits( @@ -756,13 +796,14 @@ const fn invalid_admin_configuration() -> RhiAdminServerError { RhiAdminServerError::new(RhiAdminServerErrorKind::InvalidConfiguration) } -const fn map_admin_server_error(error: AdminServerError) -> RhiAdminServerError { +const fn map_admin_server_error(error: SharedAdminServerError) -> RhiAdminServerError { let kind = match error { - AdminServerError::ListenerClone { .. } | AdminServerError::ListenerRegistration { .. } => { - RhiAdminServerErrorKind::Listener + SharedAdminServerError::ListenerClone { .. } + | SharedAdminServerError::ListenerRegistration { .. } => RhiAdminServerErrorKind::Listener, + SharedAdminServerError::Accept { .. } => RhiAdminServerErrorKind::Accept, + SharedAdminServerError::ConnectionTaskPanicked => { + RhiAdminServerErrorKind::ConnectionTaskPanicked } - AdminServerError::Accept { .. } => RhiAdminServerErrorKind::Accept, - AdminServerError::ConnectionTaskPanicked => RhiAdminServerErrorKind::ConnectionTaskPanicked, }; RhiAdminServerError::new(kind) } @@ -1732,6 +1773,16 @@ mod tests { assert!(operator_route_inventory_is_exact()); assert_eq!(RhiAdminRoute::ALL.len(), 22); assert_eq!(RhiAdminRoute::COMMON.len(), 7); + assert_eq!(RhiAdminRoute::DOMAIN.len(), 13); + assert_eq!(RhiAdminRoute::ACTIVE.len(), 20); + assert_eq!( + RhiAdminRoute::ACTIVE, + RhiAdminRoute::COMMON + .into_iter() + .chain(RhiAdminRoute::DOMAIN) + .collect::<Vec<_>>() + .as_slice() + ); let referenced = RhiAdminRoute::ALL .into_iter() .flat_map(|route| [route.request_model(), route.response_model()]) @@ -1913,7 +1964,7 @@ mod tests { const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); - type FixtureCall = (RhiAdminRoute, Option<String>, Box<[u8]>); + type FixtureCall = (RhiAdminRoute, Option<String>, Option<String>, Box<[u8]>); struct FixtureHandler { calls: Mutex<Vec<FixtureCall>>, @@ -1935,9 +1986,19 @@ mod tests { RhiAdminHandlerErrorKind::OperationIdConflict, )); } + if request + .model_bytes() + .windows(15) + .any(|bytes| bytes == b"\"cursor\":\"AAAA\"") + { + return Err(RhiAdminHandlerError::new( + RhiAdminHandlerErrorKind::InvalidCursor, + )); + } self.calls.lock().expect("calls").push(( request.route(), request.operation_id().map(str::to_owned), + request.parameter("trade_id").map(str::to_owned), request.model_bytes().into(), )); Ok(response_document(request.route())) @@ -2020,11 +2081,11 @@ mod tests { } #[tokio::test] - async fn seven_common_routes_round_trip_over_the_hardened_unix_boundary() { + async fn twenty_active_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)) + let server = RhiAdminServer::new(&configuration, Arc::clone(&handler)) .expect("production admin server") .bind(&runtime) .await @@ -2040,7 +2101,7 @@ mod tests { let client = AdminClient::new(&socket, AdminTransportLimits::DEFAULT).expect("admin client"); - for (index, route) in RhiAdminRoute::COMMON.into_iter().enumerate() { + for (index, route) in RhiAdminRoute::ACTIVE.into_iter().enumerate() { let request = sample_model(route.request_model()); let target = target_for(route, &request); let response = match route.method() { @@ -2087,10 +2148,31 @@ mod tests { assert!(response.starts_with("HTTP/1.1 400 "), "{response}"); } - let domain_target = - AdminClientTarget::new("/v1/reconciliation/status").expect("deferred domain route"); + let domain_target = AdminClientTarget::new("/v1/reconciliation/jobs?limit=201") + .expect("bounded domain route"); assert!(client.get::<Value>(&domain_target).await.is_err()); + let invalid_cursor_target = + AdminClientTarget::new("/v1/reconciliation/jobs?cursor=AAAA&limit=1") + .expect("authenticated cursor route"); + let invalid_cursor = client + .get::<Value>(&invalid_cursor_target) + .await + .expect_err("domain handler rejects unbound cursor"); + assert_eq!( + invalid_cursor + .failure() + .expect("failure envelope") + .error() + .code() + .as_str(), + "invalid_cursor" + ); + + let invalid_trade = AdminClientTarget::new("/v1/trades/not-hex/projection") + .expect("trade route target"); + assert!(client.get::<Value>(&invalid_trade).await.is_err()); + let sensitive_target = AdminClientTarget::new("/v1/identity/rekey").expect("deferred sensitive route"); assert!( @@ -2107,10 +2189,19 @@ mod tests { { 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!(calls.len(), 20); + for (index, (route, operation_id, parameter, request)) in calls.iter().enumerate() { + assert_eq!(*route, RhiAdminRoute::ACTIVE[index]); assert_eq!(operation_id.is_some(), route.is_mutation()); + assert_eq!( + parameter.is_some(), + matches!( + route, + RhiAdminRoute::TradeProjection + | RhiAdminRoute::TradeReportCurrent + | RhiAdminRoute::TradeReports + ) + ); validate_model( route.request_model(), &serde_json::from_slice(request).expect("retained request model"), diff --git a/src/lib.rs b/src/lib.rs @@ -41,9 +41,9 @@ 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, + RhiAdminRequestDocument, RhiAdminResponseDocument, RhiAdminRoute, RhiAdminRouter, + RhiAdminRouterError, RhiAdminServer, RhiAdminServerError, RhiAdminServerErrorKind, + RhiBoundAdminServer, build_rhi_admin_router, }; pub use cli_v1::{ RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1, RhiCliV1Error, diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -234,9 +234,10 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { "RhiAdminRequestDocument", "RhiAdminResponseDocument", "RhiAdminHandler", - "RhiCommonAdminServer", - "RhiBoundCommonAdminServer", - "build_rhi_common_admin_router", + "RhiAdminRouter", + "RhiAdminServer", + "RhiBoundAdminServer", + "build_rhi_admin_router", "RhiPublicationErrorKind", "RhiPublicationMode", "RhiPublicationRetryPolicy", @@ -363,13 +364,15 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { } #[test] -fn common_admin_boundary_hides_shared_transport_authority() { +fn active_admin_boundary_hides_shared_transport_authority() { for required in [ - "pub struct RhiCommonAdminRouter", - "pub struct RhiCommonAdminServer", - "pub struct RhiBoundCommonAdminServer", + "pub struct RhiAdminRouter", + "pub struct RhiAdminServer", + "pub struct RhiBoundAdminServer", "pub trait RhiAdminHandler", "pub const COMMON: [Self; 7]", + "pub const DOMAIN: [Self; 13]", + "pub const ACTIVE: [Self; 20]", ] { assert!( ADMIN.contains(required), @@ -1487,11 +1490,14 @@ 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", + "## Unix-admin boundary", "seven common RHI routes", - "Domain and sensitive routes remain deliberately", + "thirteen reconciliation, job, source", + "cursors use canonical base64url without padding", + "two identity-sensitive mutations remain unregistered", "22-route/36-model inventory", "[`admin_common.v1.json`](contracts/services_hardening/admin_common.v1.json)", + "[`admin_domain.v1.json`](contracts/services_hardening/admin_domain.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 @@ -7,7 +7,6 @@ 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() { @@ -51,7 +50,7 @@ fn common_route_inventory_is_an_exact_ordered_subset() { } #[test] -fn common_adapter_is_sealed_bounded_and_partial_by_construction() { +fn common_checkpoint_contract_remains_sealed_bounded_and_partial() { 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); @@ -64,29 +63,11 @@ fn common_adapter_is_sealed_bounded_and_partial_by_construction() { 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}`" - ); - } + assert_eq!( + common["deferred_route_groups"], + serde_json::json!([ + "domain_queries_and_mutations_step_207", + "identity_and_sensitive_mutations_step_208" + ]) + ); } diff --git a/tests/services_hardening_admin_domain.rs b/tests/services_hardening_admin_domain.rs @@ -0,0 +1,150 @@ +#![forbid(unsafe_code)] + +use std::collections::BTreeSet; + +use rhi::RhiAdminRoute; +use serde_json::Value; + +const COMMON_CONTRACT: &str = include_str!("../contracts/services_hardening/admin_common.v1.json"); +const DOMAIN_CONTRACT: &str = include_str!("../contracts/services_hardening/admin_domain.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 domain_inventory_is_exact_ordered_disjoint_and_cumulative() { + let common: Value = serde_json::from_str(COMMON_CONTRACT).expect("common admin contract"); + let domain: Value = serde_json::from_str(DOMAIN_CONTRACT).expect("domain admin contract"); + let operator: Value = serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + assert_eq!(domain["schema"], "radroots.rhi.admin-domain.v1"); + assert_eq!(domain["contract_version"], 1); + assert_eq!(domain["active_route_count"], 20); + + let common = common["registered_routes"] + .as_array() + .expect("common routes") + .iter() + .map(|value| value.as_str().expect("operation ID")) + .collect::<Vec<_>>(); + let newly_registered = domain["newly_registered_routes"] + .as_array() + .expect("domain routes") + .iter() + .map(|value| value.as_str().expect("operation ID")) + .collect::<Vec<_>>(); + assert_eq!(common.len(), 7); + assert_eq!(newly_registered.len(), 13); + assert!(common.iter().all(|route| !newly_registered.contains(route))); + + let active = common + .iter() + .chain(&newly_registered) + .copied() + .collect::<Vec<_>>(); + let governed = operator["admin"]["routes"] + .as_array() + .expect("operator routes") + .iter() + .map(|route| route["operation_id"].as_str().expect("operation ID")) + .collect::<Vec<_>>(); + let deferred = [ + "radroots.rhi.identity.rekey.v1", + "radroots.rhi.identity.replace.v1", + ]; + let expected_active = governed + .iter() + .copied() + .filter(|route| !deferred.contains(route)) + .collect::<Vec<_>>(); + assert_eq!(active, expected_active); + assert_eq!( + RhiAdminRoute::COMMON + .into_iter() + .map(RhiAdminRoute::operation_id) + .collect::<Vec<_>>(), + common + ); + assert_eq!( + RhiAdminRoute::DOMAIN + .into_iter() + .map(RhiAdminRoute::operation_id) + .collect::<Vec<_>>(), + newly_registered + ); + assert_eq!( + RhiAdminRoute::ACTIVE + .into_iter() + .map(RhiAdminRoute::operation_id) + .collect::<Vec<_>>(), + expected_active + ); + assert_eq!(active.iter().copied().collect::<BTreeSet<_>>().len(), 20); + assert_eq!( + domain["deferred_routes"], + serde_json::json!([ + "radroots.rhi.identity.rekey.v1", + "radroots.rhi.identity.replace.v1" + ]) + ); + assert_eq!( + governed + .iter() + .copied() + .filter(|route| deferred.contains(route)) + .collect::<Vec<_>>(), + deferred + ); +} + +#[test] +fn domain_adapter_is_bounded_handler_owned_and_sensitive_route_free() { + let domain: Value = serde_json::from_str(DOMAIN_CONTRACT).expect("domain admin contract"); + assert_eq!(domain["pagination"]["maximum_page_items"], 200); + assert_eq!( + domain["pagination"]["cursor_encoding"], + "canonical_base64url_no_padding" + ); + assert_eq!( + domain["pagination"]["cursor_integrity"], + "server_authenticated" + ); + assert_eq!( + domain["pagination"]["cursor_binding"], + serde_json::json!(["route", "filters", "snapshot"]) + ); + assert_eq!(domain["pagination"]["duplicate_query_items"], "reject"); + assert_eq!(domain["path_parameters"]["trade_id"]["utf8_bytes"], 32); + assert_eq!( + domain["authority"]["operation_id_conflicting_reuse"], + "reject" + ); + assert_eq!(domain["authority"]["raw_shared_router_public"], false); + assert_eq!(domain["effects"]["adapter_performs_sqlite"], false); + assert_eq!(domain["effects"]["adapter_performs_relay_io"], false); + assert_eq!(domain["effects"]["identity_mutation"], false); + + for required in [ + "pub const DOMAIN: [Self; 13]", + "pub const ACTIVE: [Self; 20]", + "for route in RhiAdminRoute::ACTIVE", + "Some((\"trade_id\", \"trade_id\"))", + "RhiAdminHandlerErrorKind::InvalidCursor", + "twenty_active_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", + "TcpListener", + "Cors", + ] { + assert!( + !ADMIN_SOURCE.contains(forbidden), + "forbidden boundary `{forbidden}`" + ); + } +}