rhi

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

commit d2ca795a81de1b4f9c6200f06b38d6723feabc58
parent 2d6e793d38e8b7383bddac60c86fd02046ceec0f
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 17:03:20 +0000

test(rhi): close Unix admin qualification wave

Freeze the final 20-route and 33-model qualification contract against the exact Lib source lock.

Exercise original-wire, version, pagination, idempotency, peer-permission, removed-route, and resource boundaries over the sealed Unix adapter.

Diffstat:
MAGENTS.md | 16+++++++++-------
MREADME | 7++++++-
Acontracts/services_hardening/admin_wave_qualification.v1.json | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/admin_v1.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mtests/package_boundary.rs | 9+++++++++
Atests/services_hardening_admin_wave_qualification.rs | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 416 insertions(+), 34 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -404,13 +404,15 @@ identity rekey/replace vocabulary, and Step 209 alone may claim the complete 20-route/33-model surface. Never expose the shared raw router, listener, JSON handler, or a caller-selected socket path. -- Through Step 208, the active and final route sets are 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. Identity rotation remains offline create-new - plus validated configuration apply and restart; Unix peer admission grants - no direct SQLite or identity-provider mutation authority. +- Through Step 209, the active and final route sets are exactly seven common + plus thirteen domain routes. The Step 209 wave contract freezes the complete + original-wire, version, pagination, idempotency, peer, removed-route, and + resource negative matrices against the exact Lib source lock. 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. Identity rotation remains offline + create-new plus validated configuration apply and restart; Unix peer + admission grants no direct SQLite or identity-provider mutation authority. - Optional TCP operations expose only cached `/livez`, `/readyz`, and `/metrics`; requests must not perform SQLite, source, relay, DNS, identity, evidence, or credential probes. diff --git a/README b/README @@ -584,10 +584,15 @@ suggested live provider authority. Identity rotation is only offline create-new envelope plus validated configuration apply and restart. Unix peer authorization remains a transport admission gate and grants neither direct SQLite nor identity-provider mutation authority. Step 209 qualifies the complete -20-route/33-model inventory. The cumulative domain contract is +20-route/33-model inventory with real Unix-socket original-wire, version, +pagination, idempotency, peer-permission, removed-route, and resource negative +matrices plus the exact source-locked shared-transport corpus. The cumulative +domain contract is [`admin_domain.v1.json`](contracts/services_hardening/admin_domain.v1.json). The offline identity correction is [`admin_identity_offline.v1.json`](contracts/services_hardening/admin_identity_offline.v1.json). +The completed admin-wave qualification is +[`admin_wave_qualification.v1.json`](contracts/services_hardening/admin_wave_qualification.v1.json). ## Sealed service-instance paths diff --git a/contracts/services_hardening/admin_wave_qualification.v1.json b/contracts/services_hardening/admin_wave_qualification.v1.json @@ -0,0 +1,104 @@ +{ + "schema": "radroots.rhi.admin-wave-qualification", + "schema_version": 1, + "contract_version": 1, + "step": 209, + "wave": "130-b", + "service": "rhi", + "final_inventory": { + "route_count": 20, + "model_count": 33, + "common_route_count": 7, + "domain_route_count": 13, + "source": "operator_contract.v1.json" + }, + "component_corpus": [ + "complete_route_and_model_inventory_matches_the_machine_contract", + "strict_response_admission_rejects_duplicates_null_and_noncanonical_bytes", + "query_and_nested_type_admission_is_exact_and_bounded", + "public_diagnostics_are_source_free_and_content_free", + "twenty_active_routes_round_trip_over_the_hardened_unix_boundary", + "production_server_projects_exact_validated_admin_limits" + ], + "live_negative_matrix": { + "original_wire": [ + "duplicate_request_field", + "nested_null", + "missing_contract_version" + ], + "version": [ + "unknown_major_path", + "contract_version_zero_every_mutation", + "contract_version_two_every_mutation" + ], + "pagination": [ + "noncanonical_limit", + "just_over_maximum_limit", + "duplicate_query_item", + "unknown_query_item", + "malformed_percent_encoding", + "noncanonical_cursor", + "handler_rejected_unbound_cursor" + ], + "idempotency": [ + "exact_operation_replay_returns_success_without_second_commit", + "conflicting_operation_reuse_rejected" + ], + "peer": [ + "owner_only_socket_mode", + "current_owner_round_trip", + "source_locked_linux_uid_or_gid_allow_and_other_deny" + ], + "removed_sensitive_routes": [ + "identity_rekey_unavailable", + "identity_replace_unavailable" + ], + "resource": [ + "request_body_just_over_limit", + "response_body_just_over_limit", + "page_limit_just_over_maximum", + "source_locked_header_query_connection_deadline_and_drain_limits" + ] + }, + "source_locked_transport_evidence": { + "repository": "https://github.com/radrootslabs/lib", + "revision": "21b11e7a5120ea949f7ad0838c746873fc73aac2", + "package": "radroots_service_host", + "corpus": [ + "serves_valid_json_with_exact_caller_correlation_and_no_web_headers", + "rejects_oversized_and_malformed_json_before_the_handler", + "rejects_invalid_mutation_envelopes_duplicates_and_nested_null_before_dispatch", + "parameterized_routes_percent_decode_bounded_values_without_service_authority", + "caller_correlation_precedes_entropy_and_survives_timeout_handoff", + "rejects_http_1_0_before_dispatch", + "request_deadline_returns_a_safe_timeout_and_cancels_the_handler_future", + "enforces_header_query_response_and_body_correlation_boundaries", + "connection_admission_never_exceeds_the_configured_limit", + "graceful_cancellation_stops_admission_and_drains_an_active_request", + "linux_process_credentials_allow_uid_or_gid_and_deny_otherwise", + "exact_positive_boundaries_are_accepted_for_every_field", + "zero_and_just_over_maximum_fail_for_every_field" + ] + }, + "invariants": { + "active_equals_final_inventory": true, + "all_models_referenced_exactly": true, + "authoritative_commit_owned_by_handler": true, + "cursor_authentication_owned_by_handler": true, + "peer_authorization_owned_by_shared_transport": true, + "adapter_performs_sqlite": false, + "adapter_performs_relay_io": false, + "adapter_performs_identity_mutation": false, + "raw_shared_transport_public": false, + "unbounded_resource": false + }, + "deferred": [ + "rcld_promotion", + "parent_pin_alignment", + "nix", + "oci", + "terminal_consumer_convergence", + "rcld-rshr-180", + "rcld-rshr-190" + ] +} diff --git a/src/admin_v1.rs b/src/admin_v1.rs @@ -498,7 +498,7 @@ impl fmt::Display for RhiAdminRouterError { impl Error for RhiAdminRouterError {} -/// Opaque RHI v1 router capability through Step 208. +/// Opaque final RHI v1 router capability through Step 209. /// /// The underlying shared-host router remains an implementation detail. The /// later runtime-composition checkpoint consumes this capability without @@ -627,7 +627,7 @@ impl fmt::Display for RhiAdminServerError { impl Error for RhiAdminServerError {} -/// Unbound RHI Unix-admin server through Step 208. +/// Unbound final RHI Unix-admin server through Step 209. /// /// Construction projects only the already-admitted Rhi configuration, seals /// the exact route inventory around the supplied domain handler, and uses the @@ -679,7 +679,7 @@ impl fmt::Debug for RhiAdminServer { } } -/// Bound RHI Unix-admin server through Step 208. +/// Bound final RHI Unix-admin server through Step 209. pub struct RhiBoundAdminServer { inner: SharedAdminServer, binding: UnixAdminSocketBinding, @@ -704,7 +704,7 @@ impl fmt::Debug for RhiBoundAdminServer { } } -/// Registers the final seven common and thirteen domain routes through Step 208. +/// Registers the final seven common and thirteen domain routes through Step 209. /// /// Live identity rekey and replace are absent by final offline-only policy. pub fn build_rhi_admin_router<H>(handler: Arc<H>) -> Result<RhiAdminRouter, RhiAdminRouterError> @@ -1919,6 +1919,7 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "macos"))] mod native { + use std::collections::BTreeMap; use std::fs; use std::path::Path; use std::sync::Mutex; @@ -1931,15 +1932,18 @@ mod tests { const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); type FixtureCall = (RhiAdminRoute, Option<String>, Option<String>, Box<[u8]>); + type FixtureOperation = (RhiAdminRoute, Box<[u8]>); struct FixtureHandler { calls: Mutex<Vec<FixtureCall>>, + operations: Mutex<BTreeMap<String, FixtureOperation>>, } impl FixtureHandler { fn new() -> Self { Self { calls: Mutex::new(Vec::new()), + operations: Mutex::new(BTreeMap::new()), } } } @@ -1947,11 +1951,6 @@ mod tests { 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, - )); - } if request .model_bytes() .windows(15) @@ -1961,6 +1960,23 @@ mod tests { RhiAdminHandlerErrorKind::InvalidCursor, )); } + if let Some(operation_id) = request.operation_id() { + let mut operations = self.operations.lock().expect("operations"); + if let Some((route, request_bytes)) = operations.get(operation_id) { + if *route != request.route() + || request_bytes.as_ref() != request.model_bytes() + { + return Err(RhiAdminHandlerError::new( + RhiAdminHandlerErrorKind::OperationIdConflict, + )); + } + return Ok(response_document(request.route())); + } + operations.insert( + operation_id.to_owned(), + (request.route(), request.model_bytes().into()), + ); + } self.calls.lock().expect("calls").push(( request.route(), request.operation_id().map(str::to_owned), @@ -2026,23 +2042,29 @@ mod tests { AdminClientTarget::new(format!("{path}?{}", serializer.finish())).expect("query target") } - async fn raw_post(socket: &Path, body: &str) -> String { + async fn raw_post(socket: &Path, path: &str, 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}", + "POST {path} 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"); + match stream.write_all(request.as_bytes()).await { + Ok(()) => {} + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset + ) => {} + Err(error) => panic!("raw request: {error}"), + } let mut response = Vec::new(); - stream - .read_to_end(&mut response) - .await - .expect("raw response"); + match stream.read_to_end(&mut response).await { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::ConnectionReset => {} + Err(error) => panic!("raw response: {error}"), + } String::from_utf8(response).expect("HTTP response") } @@ -2067,6 +2089,16 @@ mod tests { let client = AdminClient::new(&socket, AdminTransportLimits::DEFAULT).expect("admin client"); + use std::os::unix::fs::PermissionsExt as _; + assert_eq!( + fs::metadata(&socket) + .expect("admin socket metadata") + .permissions() + .mode() + & 0o777, + 0o600 + ); + for (index, route) in RhiAdminRoute::ACTIVE.into_iter().enumerate() { let request = sample_model(route.request_model()); let target = target_for(route, &request); @@ -2085,17 +2117,31 @@ mod tests { .expect("route response model"); } - let conflict_target = + let replay_target = AdminClientTarget::new("/v1/state/backup").expect("mutation target"); - let conflict_error = client + let replay_operation = AdminOperationId::new("operation-5").expect("operation ID"); + let replay_request = sample_model("state_backup_request_v1"); + client .mutate::<_, Value>( - &conflict_target, - AdminOperationId::new("conflict").expect("operation ID"), + &replay_target, + replay_operation.clone(), None, - sample_model("state_backup_request_v1"), + &replay_request, ) .await - .expect_err("operation conflict"); + .expect("exact operation replay"); + let mut conflicting_request = replay_request; + conflicting_request + .as_object_mut() + .expect("backup request") + .insert( + "target_path".to_owned(), + Value::String("/tmp/different-backup".to_owned()), + ); + let conflict_error = client + .mutate::<_, Value>(&replay_target, replay_operation, None, &conflicting_request) + .await + .expect_err("conflicting operation reuse"); assert_eq!( conflict_error .failure() @@ -2110,10 +2156,42 @@ mod tests { 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; + let response = raw_post(&socket, "/v1/state/backup", body).await; assert!(response.starts_with("HTTP/1.1 400 "), "{response}"); } + for (index, route) in RhiAdminRoute::ACTIVE + .into_iter() + .filter(|route| route.is_mutation()) + .enumerate() + { + let request = sample_model(route.request_model()); + for version in [0, 2] { + let body = serde_json::json!({ + "contract_version": version, + "operation_id": format!("invalid-version-{index}-{version}"), + "request": request, + }) + .to_string(); + let response = raw_post(&socket, route.path(), &body).await; + assert!(response.starts_with("HTTP/1.1 400 "), "{response}"); + } + let missing_version = serde_json::json!({ + "operation_id": format!("missing-version-{index}"), + "request": request, + }) + .to_string(); + let response = raw_post(&socket, route.path(), &missing_version).await; + assert!(response.starts_with("HTTP/1.1 400 "), "{response}"); + } + + let request_limit = + usize::try_from(AdminTransportLimits::DEFAULT.request_body_utf8_bytes()) + .expect("request limit"); + let oversized = " ".repeat(request_limit + 1); + let response = raw_post(&socket, "/v1/state/backup", &oversized).await; + assert!(response.starts_with("HTTP/1.1 413 "), "{response}"); + let domain_target = AdminClientTarget::new("/v1/reconciliation/jobs?limit=201") .expect("bounded domain route"); assert!(client.get::<Value>(&domain_target).await.is_err()); @@ -2139,6 +2217,8 @@ mod tests { .expect("trade route target"); assert!(client.get::<Value>(&invalid_trade).await.is_err()); + assert!(AdminClientTarget::new("/v2/status").is_err()); + for (index, path) in ["/v1/identity/rekey", "/v1/identity/replace"] .into_iter() .enumerate() diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -7,6 +7,8 @@ const ROOT: &str = include_str!("../src/lib.rs"); const ADMIN: &str = include_str!("../src/admin_v1.rs"); const ADMIN_IDENTITY_OFFLINE_CONTRACT: &str = include_str!("../contracts/services_hardening/admin_identity_offline.v1.json"); +const ADMIN_WAVE_QUALIFICATION_CONTRACT: &str = + include_str!("../contracts/services_hardening/admin_wave_qualification.v1.json"); 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"); @@ -387,6 +389,12 @@ fn active_admin_boundary_hides_shared_transport_authority() { assert_eq!(offline["final_inventory"]["route_count"], 20); assert_eq!(offline["final_inventory"]["model_count"], 33); assert_eq!(offline["identity_rotation"]["unix_admin_mutation"], false); + let qualification: serde_json::Value = serde_json::from_str(ADMIN_WAVE_QUALIFICATION_CONTRACT) + .expect("admin wave qualification contract"); + assert_eq!(qualification["step"], 209); + assert_eq!(qualification["wave"], "130-b"); + assert_eq!(qualification["final_inventory"]["route_count"], 20); + assert_eq!(qualification["final_inventory"]["model_count"], 33); for forbidden in ["IdentityRekey", "IdentityReplace"] { assert!(!ADMIN.contains(forbidden)); assert!(!PUBLIC_API.contains(forbidden)); @@ -1511,6 +1519,7 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() { "[`admin_common.v1.json`](contracts/services_hardening/admin_common.v1.json)", "[`admin_domain.v1.json`](contracts/services_hardening/admin_domain.v1.json)", "[`admin_identity_offline.v1.json`](contracts/services_hardening/admin_identity_offline.v1.json)", + "[`admin_wave_qualification.v1.json`](contracts/services_hardening/admin_wave_qualification.v1.json)", ] { assert!(README.contains(required), "README is missing {required}"); } diff --git a/tests/services_hardening_admin_wave_qualification.rs b/tests/services_hardening_admin_wave_qualification.rs @@ -0,0 +1,182 @@ +#![forbid(unsafe_code)] + +use std::collections::BTreeSet; + +use rhi::RhiAdminRoute; +use serde_json::{Value, json}; + +const CONTRACT: &str = + include_str!("../contracts/services_hardening/admin_wave_qualification.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"); +const MANIFEST: &str = include_str!("../Cargo.toml"); +const ROOT_SOURCE: &str = include_str!("../src/lib.rs"); + +#[test] +fn step209_machine_contract_freezes_the_complete_admin_wave() { + let contract: Value = serde_json::from_str(CONTRACT).expect("qualification contract"); + let operator: Value = serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + assert_eq!(contract["schema"], "radroots.rhi.admin-wave-qualification"); + assert_eq!(contract["schema_version"], 1); + assert_eq!(contract["contract_version"], 1); + assert_eq!(contract["step"], 209); + assert_eq!(contract["wave"], "130-b"); + assert_eq!(contract["service"], "rhi"); + assert_eq!(contract["final_inventory"]["route_count"], 20); + assert_eq!(contract["final_inventory"]["model_count"], 33); + assert_eq!(contract["final_inventory"]["common_route_count"], 7); + assert_eq!(contract["final_inventory"]["domain_route_count"], 13); + + let routes = operator["admin"]["routes"].as_array().expect("routes"); + let models = operator["admin"]["models"].as_object().expect("models"); + assert_eq!(routes.len(), 20); + assert_eq!(models.len(), 33); + assert_eq!(RhiAdminRoute::ALL, RhiAdminRoute::ACTIVE); + assert_eq!( + RhiAdminRoute::COMMON + .into_iter() + .chain(RhiAdminRoute::DOMAIN) + .collect::<Vec<_>>(), + RhiAdminRoute::ALL + ); + let referenced = routes + .iter() + .flat_map(|route| { + ["request_model", "response_model"] + .map(|field| route[field].as_str().expect("model reference")) + }) + .collect::<BTreeSet<_>>(); + assert_eq!( + referenced, + models.keys().map(String::as_str).collect::<BTreeSet<_>>() + ); + + assert_eq!( + contract["live_negative_matrix"], + json!({ + "original_wire": [ + "duplicate_request_field", + "nested_null", + "missing_contract_version" + ], + "version": [ + "unknown_major_path", + "contract_version_zero_every_mutation", + "contract_version_two_every_mutation" + ], + "pagination": [ + "noncanonical_limit", + "just_over_maximum_limit", + "duplicate_query_item", + "unknown_query_item", + "malformed_percent_encoding", + "noncanonical_cursor", + "handler_rejected_unbound_cursor" + ], + "idempotency": [ + "exact_operation_replay_returns_success_without_second_commit", + "conflicting_operation_reuse_rejected" + ], + "peer": [ + "owner_only_socket_mode", + "current_owner_round_trip", + "source_locked_linux_uid_or_gid_allow_and_other_deny" + ], + "removed_sensitive_routes": [ + "identity_rekey_unavailable", + "identity_replace_unavailable" + ], + "resource": [ + "request_body_just_over_limit", + "response_body_just_over_limit", + "page_limit_just_over_maximum", + "source_locked_header_query_connection_deadline_and_drain_limits" + ] + }) + ); +} + +#[test] +fn qualification_is_executable_source_locked_and_authority_safe() { + let contract: Value = serde_json::from_str(CONTRACT).expect("qualification contract"); + for test in contract["component_corpus"] + .as_array() + .expect("component corpus") + { + let test = test.as_str().expect("test name"); + assert!( + ADMIN_SOURCE.contains(test), + "missing component test `{test}`" + ); + } + + let revision = contract["source_locked_transport_evidence"]["revision"] + .as_str() + .expect("Lib revision"); + assert_eq!(revision.len(), 40); + assert!( + revision + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + ); + assert!(MANIFEST.contains(&format!("rev = \"{revision}\""))); + assert!( + MANIFEST + .contains("radroots_service_host = { git = \"https://github.com/radrootslabs/lib\"") + ); + assert_eq!( + contract["source_locked_transport_evidence"]["corpus"], + json!([ + "serves_valid_json_with_exact_caller_correlation_and_no_web_headers", + "rejects_oversized_and_malformed_json_before_the_handler", + "rejects_invalid_mutation_envelopes_duplicates_and_nested_null_before_dispatch", + "parameterized_routes_percent_decode_bounded_values_without_service_authority", + "caller_correlation_precedes_entropy_and_survives_timeout_handoff", + "rejects_http_1_0_before_dispatch", + "request_deadline_returns_a_safe_timeout_and_cancels_the_handler_future", + "enforces_header_query_response_and_body_correlation_boundaries", + "connection_admission_never_exceeds_the_configured_limit", + "graceful_cancellation_stops_admission_and_drains_an_active_request", + "linux_process_credentials_allow_uid_or_gid_and_deny_otherwise", + "exact_positive_boundaries_are_accepted_for_every_field", + "zero_and_just_over_maximum_fail_for_every_field" + ]) + ); + + for invariant in [ + "active_equals_final_inventory", + "all_models_referenced_exactly", + "authoritative_commit_owned_by_handler", + "cursor_authentication_owned_by_handler", + "peer_authorization_owned_by_shared_transport", + ] { + assert_eq!(contract["invariants"][invariant], true, "{invariant}"); + } + for invariant in [ + "adapter_performs_sqlite", + "adapter_performs_relay_io", + "adapter_performs_identity_mutation", + "raw_shared_transport_public", + "unbounded_resource", + ] { + assert_eq!(contract["invariants"][invariant], false, "{invariant}"); + } + for forbidden in [ + "IdentityRekey", + "IdentityReplace", + "pub fn into_inner", + "pub fn router", + "pub fn listener", + "sqlx::", + ] { + assert!( + !ADMIN_SOURCE.contains(forbidden), + "forbidden admin surface `{forbidden}`" + ); + assert!( + !ROOT_SOURCE.contains(forbidden), + "forbidden root surface `{forbidden}`" + ); + } +}