rhi

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

commit 2d6e793d38e8b7383bddac60c86fd02046ceec0f
parent 18222d3a4cfc2ed21d93db7e2d5d7e5377918aaf
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 16:42:53 +0000

refactor(rhi): remove live identity mutations

Remove the never-registered live rekey and replace routes from the final RHI operator contract.

Freeze offline create-new, configuration-apply, restart rotation and prove peer authorization grants no direct identity or SQLite mutation authority.

Diffstat:
MAGENTS.md | 22++++++++++++----------
MREADME | 12+++++++++---
Mcontracts/api_baselines/rhi.txt | 4+---
Mcontracts/services_hardening/admin_common.v1.json | 8++++----
Mcontracts/services_hardening/admin_domain.v1.json | 6+++---
Acontracts/services_hardening/admin_identity_offline.v1.json | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/services_hardening/operator_contract.v1.json | 22+++-------------------
Msrc/admin_v1.rs | 88+++++++++++++++++++++++++++----------------------------------------------------
Mtests/package_boundary.rs | 17+++++++++++++++--
Mtests/services_hardening_admin_common.rs | 8++++----
Mtests/services_hardening_admin_domain.rs | 21+++------------------
Atests/services_hardening_admin_identity_offline.rs | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/services_hardening_operator_contract.rs | 32+++++++++++++++++++++-----------
13 files changed, 264 insertions(+), 135 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -399,16 +399,18 @@ 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. -- 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. + operator contract. Step 206 owns the seven common routes and Step 207 owns + the thirteen domain routes. Step 208 removes the stale, never-registered live + 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. - 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 @@ -578,10 +578,16 @@ bound by the handler to the same route, filters, and snapshot. Every decoded 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 +Step 208 removes the two never-registered live identity rekey/replace routes, +their three mutation-only models, and the unused operator types that could have +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 [`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). ## Sealed service-instance paths diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt @@ -39,8 +39,6 @@ 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 @@ -61,7 +59,7 @@ 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::ALL: [Self; 20] pub const rhi::RhiAdminRoute::COMMON: [Self; 7] pub const rhi::RhiAdminRoute::DOMAIN: [Self; 13] pub const fn rhi::RhiAdminRoute::is_mutation(self) -> bool diff --git a/contracts/services_hardening/admin_common.v1.json b/contracts/services_hardening/admin_common.v1.json @@ -4,8 +4,8 @@ "service": "rhi", "shared_transport": "radroots_service_host", "final_inventory": { - "route_count": 22, - "model_count": 36, + "route_count": 20, + "model_count": 33, "source": "operator_contract.v1.json" }, "registered_routes": [ @@ -17,9 +17,9 @@ "radroots.rhi.state.backup.create.v1", "radroots.rhi.metrics.snapshot.get.v1" ], - "deferred_route_groups": [ + "historical_deferred_route_groups": [ "domain_queries_and_mutations_step_207", - "identity_and_sensitive_mutations_step_208" + "reserved_identity_mutations_removed_step_208" ], "authority": { "raw_shared_router_public": false, diff --git a/contracts/services_hardening/admin_domain.v1.json b/contracts/services_hardening/admin_domain.v1.json @@ -4,8 +4,8 @@ "service": "rhi", "shared_transport": "radroots_service_host", "final_inventory": { - "route_count": 22, - "model_count": 36, + "route_count": 20, + "model_count": 33, "source": "operator_contract.v1.json" }, "active_route_count": 20, @@ -24,7 +24,7 @@ "radroots.rhi.presence.render.v1", "radroots.rhi.presence.refresh.v1" ], - "deferred_routes": [ + "removed_live_identity_routes": [ "radroots.rhi.identity.rekey.v1", "radroots.rhi.identity.replace.v1" ], diff --git a/contracts/services_hardening/admin_identity_offline.v1.json b/contracts/services_hardening/admin_identity_offline.v1.json @@ -0,0 +1,58 @@ +{ + "schema": "radroots.rhi.admin-identity-offline.v1", + "contract_version": 1, + "service": "rhi", + "final_inventory": { + "route_count": 20, + "model_count": 33, + "source": "operator_contract.v1.json" + }, + "removed_live_routes": [ + { + "method": "POST", + "path": "/v1/identity/rekey", + "operation_id": "radroots.rhi.identity.rekey.v1" + }, + { + "method": "POST", + "path": "/v1/identity/replace", + "operation_id": "radroots.rhi.identity.replace.v1" + } + ], + "removed_models": [ + "identity_rekey_request_v1", + "identity_replace_request_v1", + "identity_mutation_receipt_v1" + ], + "removed_operator_types": [ + "credential_reference", + "encrypted_file_provider", + "identity_provider_replacement", + "encrypted_file_replacement" + ], + "identity_rotation": { + "mode": "offline_create_new_configuration_apply_restart", + "in_place_overwrite": false, + "ordinary_run_generation": false, + "unix_admin_mutation": false + }, + "peer_authorization": { + "owned_by": "radroots_service_host", + "applies_before_route_dispatch": true, + "grants_identity_provider_authority": false, + "grants_direct_sqlite_authority": false + }, + "negative_requirements": { + "removed_paths_unavailable": true, + "removed_operation_ids_absent": true, + "removed_models_absent": true, + "raw_shared_router_public": false, + "raw_listener_public": false, + "caller_selected_socket_path": false + }, + "deferred": { + "nix": true, + "oci": true, + "terminal_consumer_convergence": true + } +} diff --git a/contracts/services_hardening/operator_contract.v1.json b/contracts/services_hardening/operator_contract.v1.json @@ -17,8 +17,6 @@ { "method": "GET", "path": "/v1/status", "operation_id": "radroots.rhi.status.get.v1", "request_model": "empty", "response_model": "service_status_v1", "mutation": false }, { "method": "GET", "path": "/v1/config/effective", "operation_id": "radroots.rhi.config.effective.get.v1", "request_model": "empty", "response_model": "effective_config_v1", "mutation": false }, { "method": "GET", "path": "/v1/identity/status", "operation_id": "radroots.rhi.identity.status.get.v1", "request_model": "identity_status_query_v1", "response_model": "identity_status_v1", "mutation": false }, - { "method": "POST", "path": "/v1/identity/rekey", "operation_id": "radroots.rhi.identity.rekey.v1", "request_model": "identity_rekey_request_v1", "response_model": "identity_mutation_receipt_v1", "mutation": true }, - { "method": "POST", "path": "/v1/identity/replace", "operation_id": "radroots.rhi.identity.replace.v1", "request_model": "identity_replace_request_v1", "response_model": "identity_mutation_receipt_v1", "mutation": true }, { "method": "GET", "path": "/v1/identity/public", "operation_id": "radroots.rhi.identity.public.get.v1", "request_model": "identity_public_query_v1", "response_model": "identity_public_v1", "mutation": false }, { "method": "GET", "path": "/v1/state/status", "operation_id": "radroots.rhi.state.status.get.v1", "request_model": "empty", "response_model": "state_status_v1", "mutation": false }, { "method": "POST", "path": "/v1/state/backup", "operation_id": "radroots.rhi.state.backup.create.v1", "request_model": "state_backup_request_v1", "response_model": "state_backup_receipt_v1", "mutation": true }, @@ -53,14 +51,9 @@ "roles": [ { "id": "service", "required": true, "disabled_allowed": false, "providers": ["encrypted_file"] } ], - "rekey_provider": "encrypted_file_only", - "replace_provider_variants": { - "discriminator": "provider", - "encrypted_file": { - "required_fields": ["provider", "envelope_path", "credential_reference", "expected_public_key"], - "provider_value": "encrypted_file" - } - } + "rotation_mode": "offline_create_new_configuration_apply_restart", + "live_rekey_route": false, + "live_replace_route": false }, "types": { "rhi_literal": { "kind": "literal", "value": "rhi" }, @@ -84,13 +77,9 @@ "report_contract_literal": { "kind": "literal", "value": "radroots.rhi.evidence_attestation.v1" }, "attestation_method_literal": { "kind": "literal", "value": "signed_evidence_snapshot" }, "absolute_path": { "kind": "string", "minimum_utf8_bytes": 1, "maximum_utf8_bytes": 4096, "pattern": "^/" }, - "credential_reference": { "kind": "string", "minimum_utf8_bytes": 1, "maximum_utf8_bytes": 256 }, "confirmation": { "kind": "literal", "value": "confirm" }, "identity_role": { "kind": "enum", "values": ["service"] }, - "encrypted_file_provider": { "kind": "literal", "value": "encrypted_file" }, "identity_provider_kind": { "kind": "enum", "values": ["encrypted_file"] }, - "identity_provider_replacement": { "kind": "tagged_union", "discriminator": "provider", "variants": ["encrypted_file_replacement"] }, - "encrypted_file_replacement": { "kind": "closed_object", "fields": { "provider": "encrypted_file_provider", "envelope_path": "absolute_path", "credential_reference": "credential_reference", "expected_public_key": "public_key_hex" } }, "service_phase": { "kind": "enum", "values": ["starting", "ready", "degraded", "unready", "stopping", "failed"] }, "uptime_millis": { "kind": "integer", "minimum": 0, "maximum": 18446744073709551615, "unit": "milliseconds_from_injected_monotonic_clock" }, "integrity_state": { "kind": "enum", "values": ["verified", "verification_required", "failed"] }, @@ -145,9 +134,6 @@ "effective_config_v1": { "fields": { "schema": { "type": "rhi_config_schema_literal", "presence": "required" }, "schema_version": { "type": "contract_version_literal", "presence": "required" }, "provenance": { "type": "config_provenance", "presence": "required" }, "redacted_config": { "type": "redacted_config", "presence": "required" }, "config_digest": { "type": "sha256_hex", "presence": "required" }, "policy_digest": { "type": "sha256_hex", "presence": "required" } } }, "identity_status_query_v1": { "fields": { "role": { "type": "identity_role", "presence": "required" } } }, "identity_status_v1": { "fields": { "role": { "type": "identity_role", "presence": "required" }, "provider": { "type": "identity_provider_kind", "presence": "required" }, "configured": { "type": "bool", "presence": "required" }, "available": { "type": "bool", "presence": "required" }, "generation": { "type": "u64", "presence": "required" }, "reason_codes": { "type": "reason_codes", "presence": "required" }, "public_key": { "type": "public_key_hex", "presence": "optional" } } }, - "identity_rekey_request_v1": { "fields": { "role": { "type": "identity_role", "presence": "required" }, "provider": { "type": "encrypted_file_provider", "presence": "required" }, "expected_generation": { "type": "u64", "presence": "required" }, "new_credential_reference": { "type": "credential_reference", "presence": "required" }, "confirmation": { "type": "confirmation", "presence": "required" } } }, - "identity_replace_request_v1": { "fields": { "role": { "type": "identity_role", "presence": "required" }, "expected_generation": { "type": "u64", "presence": "required" }, "replacement": { "type": "identity_provider_replacement", "presence": "required" }, "confirmation": { "type": "confirmation", "presence": "required" } } }, - "identity_mutation_receipt_v1": { "fields": { "operation_id": { "type": "operation_id", "presence": "required" }, "role": { "type": "identity_role", "presence": "required" }, "previous_public_key": { "type": "public_key_hex", "presence": "required" }, "current_public_key": { "type": "public_key_hex", "presence": "required" }, "generation": { "type": "u64", "presence": "required" } } }, "identity_public_query_v1": { "fields": { "role": { "type": "identity_role", "presence": "required" } } }, "identity_public_v1": { "fields": { "role": { "type": "identity_role", "presence": "required" }, "public_key": { "type": "public_key_hex", "presence": "required" }, "generation": { "type": "u64", "presence": "required" } } }, "state_status_v1": { "fields": { "schema_version": { "type": "positive_u32", "presence": "required" }, "generation": { "type": "u64", "presence": "required" }, "integrity": { "type": "integrity_state", "presence": "required" }, "writer_lock": { "type": "writer_lock_state", "presence": "required" }, "backup_eligible": { "type": "bool", "presence": "required" }, "reason_codes": { "type": "reason_codes", "presence": "required" } } }, @@ -214,8 +200,6 @@ "success_visibility": "response_only_after_local_authoritative_commit", "relay_submission_or_delivery_required_for_success": false, "committed_effects": { - "radroots.rhi.identity.rekey.v1": "identity_generation_credential_binding_and_operation_audit", - "radroots.rhi.identity.replace.v1": "identity_generation_public_key_binding_and_operation_audit", "radroots.rhi.state.backup.create.v1": "verified_snapshot_manifest_digest_and_operation_audit", "radroots.rhi.reconciliation.refresh.v1": "generation_fenced_durable_job_schedule_and_operation_audit", "radroots.rhi.publication.retry.v1": "same_committed_exact_bytes_existing_target_set_retry_schedule_and_operation_audit", diff --git a/src/admin_v1.rs b/src/admin_v1.rs @@ -38,8 +38,6 @@ pub enum RhiAdminRoute { Status, EffectiveConfig, IdentityStatus, - IdentityRekey, - IdentityReplace, IdentityPublic, StateStatus, StateBackup, @@ -61,12 +59,10 @@ pub enum RhiAdminRoute { impl RhiAdminRoute { /// Complete final machine-governed route inventory. - pub const ALL: [Self; 22] = [ + pub const ALL: [Self; 20] = [ Self::Status, Self::EffectiveConfig, Self::IdentityStatus, - Self::IdentityRekey, - Self::IdentityReplace, Self::IdentityPublic, Self::StateStatus, Self::StateBackup, @@ -114,29 +110,8 @@ impl RhiAdminRoute { 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, - ]; + /// Routes admitted through Step 208, in final machine-contract order. + pub const ACTIVE: [Self; 20] = Self::ALL; #[must_use] pub const fn method(self) -> RhiAdminMethod { @@ -157,8 +132,6 @@ impl RhiAdminRoute { | Self::PublicationTargets | Self::PresenceDesired => RhiAdminMethod::Get, Self::StateBackup - | Self::IdentityRekey - | Self::IdentityReplace | Self::ReconciliationRefresh | Self::PublicationRetry | Self::PresenceRender @@ -172,8 +145,6 @@ impl RhiAdminRoute { 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", @@ -200,8 +171,6 @@ impl RhiAdminRoute { 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", @@ -234,8 +203,6 @@ impl RhiAdminRoute { | 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", @@ -256,7 +223,6 @@ impl RhiAdminRoute { 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", @@ -532,7 +498,7 @@ impl fmt::Display for RhiAdminRouterError { impl Error for RhiAdminRouterError {} -/// Opaque RHI v1 router capability through Step 207. +/// Opaque RHI v1 router capability through Step 208. /// /// The underlying shared-host router remains an implementation detail. The /// later runtime-composition checkpoint consumes this capability without @@ -661,7 +627,7 @@ impl fmt::Display for RhiAdminServerError { impl Error for RhiAdminServerError {} -/// Unbound RHI Unix-admin server through Step 207. +/// Unbound RHI Unix-admin server through Step 208. /// /// Construction projects only the already-admitted Rhi configuration, seals /// the exact route inventory around the supplied domain handler, and uses the @@ -713,7 +679,7 @@ impl fmt::Debug for RhiAdminServer { } } -/// Bound RHI Unix-admin server through Step 207. +/// Bound RHI Unix-admin server through Step 208. pub struct RhiBoundAdminServer { inner: SharedAdminServer, binding: UnixAdminSocketBinding, @@ -738,9 +704,9 @@ impl fmt::Debug for RhiBoundAdminServer { } } -/// Registers the seven common and thirteen domain routes owned through Step 207. +/// Registers the final seven common and thirteen domain routes through Step 208. /// -/// The two sensitive identity mutations remain unregistered until Step 208. +/// 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> where H: RhiAdminHandler, @@ -1065,7 +1031,7 @@ fn operator_route_inventory_is_exact() -> bool { return false; }; routes.len() == RhiAdminRoute::ALL.len() - && models.len() == 36 + && models.len() == 33 && admin .pointer("/model_wire_contract/response_body_max_utf8_bytes") .and_then(Value::as_u64) @@ -1771,7 +1737,7 @@ mod tests { #[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::ALL.len(), 20); assert_eq!(RhiAdminRoute::COMMON.len(), 7); assert_eq!(RhiAdminRoute::DOMAIN.len(), 13); assert_eq!(RhiAdminRoute::ACTIVE.len(), 20); @@ -1794,7 +1760,7 @@ mod tests { .map(String::as_str) .collect::<BTreeSet<_>>(); assert_eq!(referenced, governed); - assert_eq!(governed.len(), 36); + assert_eq!(governed.len(), 33); assert!( RhiAdminRoute::COMMON .into_iter() @@ -2173,19 +2139,25 @@ mod tests { .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!( - client - .mutate::<_, Value>( - &sensitive_target, - AdminOperationId::new("deferred-identity").expect("operation ID"), - None, - sample_model("identity_rekey_request_v1"), - ) - .await - .is_err() - ); + for (index, path) in ["/v1/identity/rekey", "/v1/identity/replace"] + .into_iter() + .enumerate() + { + let removed_target = + AdminClientTarget::new(path).expect("removed live identity route"); + assert!( + client + .mutate::<_, Value>( + &removed_target, + AdminOperationId::new(format!("removed-identity-{index}")) + .expect("operation ID"), + None, + serde_json::json!({}), + ) + .await + .is_err() + ); + } { let calls = handler.calls.lock().expect("calls"); diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -5,6 +5,8 @@ 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 ADMIN_IDENTITY_OFFLINE_CONTRACT: &str = + include_str!("../contracts/services_hardening/admin_identity_offline.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"); @@ -372,6 +374,7 @@ fn active_admin_boundary_hides_shared_transport_authority() { "pub trait RhiAdminHandler", "pub const COMMON: [Self; 7]", "pub const DOMAIN: [Self; 13]", + "pub const ALL: [Self; 20]", "pub const ACTIVE: [Self; 20]", ] { assert!( @@ -379,6 +382,15 @@ fn active_admin_boundary_hides_shared_transport_authority() { "missing common admin boundary `{required}`" ); } + let offline: serde_json::Value = + serde_json::from_str(ADMIN_IDENTITY_OFFLINE_CONTRACT).expect("offline identity contract"); + 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); + for forbidden in ["IdentityRekey", "IdentityReplace"] { + assert!(!ADMIN.contains(forbidden)); + assert!(!PUBLIC_API.contains(forbidden)); + } for forbidden in [ "pub fn into_inner", "pub fn router", @@ -1494,10 +1506,11 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() { "seven common RHI routes", "thirteen reconciliation, job, source", "cursors use canonical base64url without padding", - "two identity-sensitive mutations remain unregistered", - "22-route/36-model inventory", + "Step 208 removes the two never-registered live identity rekey/replace routes", + "20-route/33-model inventory", "[`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)", ] { assert!(README.contains(required), "README is missing {required}"); } diff --git a/tests/services_hardening_admin_common.rs b/tests/services_hardening_admin_common.rs @@ -14,8 +14,8 @@ fn common_route_inventory_is_an_exact_ordered_subset() { 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); + assert_eq!(common["final_inventory"]["route_count"], 20); + assert_eq!(common["final_inventory"]["model_count"], 33); let registered = common["registered_routes"] .as_array() @@ -64,10 +64,10 @@ fn common_checkpoint_contract_remains_sealed_bounded_and_partial() { assert_eq!(common["effects"]["bind_spawns_task"], false); assert_eq!(common["effects"]["tcp_admin"], false); assert_eq!( - common["deferred_route_groups"], + common["historical_deferred_route_groups"], serde_json::json!([ "domain_queries_and_mutations_step_207", - "identity_and_sensitive_mutations_step_208" + "reserved_identity_mutations_removed_step_208" ]) ); } diff --git a/tests/services_hardening_admin_domain.rs b/tests/services_hardening_admin_domain.rs @@ -47,15 +47,7 @@ fn domain_inventory_is_exact_ordered_disjoint_and_cumulative() { .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<_>>(); + let expected_active = governed.clone(); assert_eq!(active, expected_active); assert_eq!( RhiAdminRoute::COMMON @@ -80,20 +72,13 @@ fn domain_inventory_is_exact_ordered_disjoint_and_cumulative() { ); assert_eq!(active.iter().copied().collect::<BTreeSet<_>>().len(), 20); assert_eq!( - domain["deferred_routes"], + domain["removed_live_identity_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 - ); + assert_eq!(governed.len(), 20); } #[test] diff --git a/tests/services_hardening_admin_identity_offline.rs b/tests/services_hardening_admin_identity_offline.rs @@ -0,0 +1,101 @@ +#![forbid(unsafe_code)] + +use rhi::RhiAdminRoute; +use serde_json::Value; + +const OFFLINE_IDENTITY_CONTRACT: &str = + include_str!("../contracts/services_hardening/admin_identity_offline.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 ROOT_SOURCE: &str = include_str!("../src/lib.rs"); + +#[test] +fn final_identity_policy_is_offline_only_and_inventory_is_exact() { + let policy: Value = + serde_json::from_str(OFFLINE_IDENTITY_CONTRACT).expect("offline identity contract"); + let operator: Value = serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + + assert_eq!(policy["schema"], "radroots.rhi.admin-identity-offline.v1"); + assert_eq!(policy["contract_version"], 1); + assert_eq!(policy["final_inventory"]["route_count"], 20); + assert_eq!(policy["final_inventory"]["model_count"], 33); + assert_eq!( + policy["identity_rotation"]["mode"], + "offline_create_new_configuration_apply_restart" + ); + assert_eq!(policy["identity_rotation"]["unix_admin_mutation"], false); + assert_eq!( + policy["peer_authorization"]["grants_identity_provider_authority"], + false + ); + assert_eq!( + policy["peer_authorization"]["grants_direct_sqlite_authority"], + false + ); + + let routes = operator["admin"]["routes"].as_array().expect("routes"); + let models = operator["admin"]["models"].as_object().expect("models"); + let types = operator["admin"]["types"].as_object().expect("types"); + assert_eq!(routes.len(), 20); + assert_eq!(models.len(), 33); + assert_eq!(RhiAdminRoute::ALL.len(), 20); + assert_eq!(RhiAdminRoute::ACTIVE, RhiAdminRoute::ALL); + + for removed in policy["removed_live_routes"] + .as_array() + .expect("removed routes") + { + let path = removed["path"].as_str().expect("removed path"); + let operation_id = removed["operation_id"] + .as_str() + .expect("removed operation ID"); + assert!(routes.iter().all(|route| route["path"] != path)); + assert!( + routes + .iter() + .all(|route| route["operation_id"] != operation_id) + ); + } + for removed in policy["removed_models"].as_array().expect("removed models") { + assert!(!models.contains_key(removed.as_str().expect("model name"))); + } + for removed in policy["removed_operator_types"] + .as_array() + .expect("removed operator types") + { + assert!(!types.contains_key(removed.as_str().expect("type name"))); + } +} + +#[test] +fn removed_live_identity_surface_cannot_reenter_the_adapter_or_root_api() { + for forbidden in [ + "IdentityRekey", + "IdentityReplace", + "identity_rekey_request_v1", + "identity_replace_request_v1", + "identity_mutation_receipt_v1", + "build_rhi_identity", + "direct_sqlite", + ] { + assert!( + !ADMIN_SOURCE.contains(forbidden), + "forbidden admin surface `{forbidden}`" + ); + assert!( + !ROOT_SOURCE.contains(forbidden), + "forbidden root surface `{forbidden}`" + ); + } + for required in [ + "pub const ALL: [Self; 20]", + "pub const ACTIVE: [Self; 20] = Self::ALL", + "Live identity rekey and replace are absent by final offline-only policy", + ] { + assert!( + ADMIN_SOURCE.contains(required), + "missing guard `{required}`" + ); + } +} diff --git a/tests/services_hardening_operator_contract.rs b/tests/services_hardening_operator_contract.rs @@ -82,8 +82,6 @@ fn admin_inventory_is_closed_unique_and_model_complete() { "GET|/v1/status|radroots.rhi.status.get.v1|empty|service_status_v1|false", "GET|/v1/config/effective|radroots.rhi.config.effective.get.v1|empty|effective_config_v1|false", "GET|/v1/identity/status|radroots.rhi.identity.status.get.v1|identity_status_query_v1|identity_status_v1|false", - "POST|/v1/identity/rekey|radroots.rhi.identity.rekey.v1|identity_rekey_request_v1|identity_mutation_receipt_v1|true", - "POST|/v1/identity/replace|radroots.rhi.identity.replace.v1|identity_replace_request_v1|identity_mutation_receipt_v1|true", "GET|/v1/identity/public|radroots.rhi.identity.public.get.v1|identity_public_query_v1|identity_public_v1|false", "GET|/v1/state/status|radroots.rhi.state.status.get.v1|empty|state_status_v1|false", "POST|/v1/state/backup|radroots.rhi.state.backup.create.v1|state_backup_request_v1|state_backup_receipt_v1|true", @@ -103,6 +101,7 @@ fn admin_inventory_is_closed_unique_and_model_complete() { "POST|/v1/presence/refresh|radroots.rhi.presence.refresh.v1|presence_refresh_request_v1|presence_refresh_receipt_v1|true" ] ); + assert_eq!(routes.len(), 20); let route_keys = routes .iter() .map(|route| format!("{} {}", route["method"], route["path"])) @@ -121,6 +120,7 @@ fn admin_inventory_is_closed_unique_and_model_complete() { let models = value["admin"]["models"].as_object().expect("models"); let types = value["admin"]["types"].as_object().expect("types"); + assert_eq!(models.len(), 33); for route in routes { for key in ["request_model", "response_model"] { let model = route[key].as_str().expect("model reference"); @@ -195,16 +195,26 @@ fn admin_inventory_is_closed_unique_and_model_complete() { value["admin"]["identity_contract"], serde_json::json!({ "roles": [{ "id": "service", "required": true, "disabled_allowed": false, "providers": ["encrypted_file"] }], - "rekey_provider": "encrypted_file_only", - "replace_provider_variants": { - "discriminator": "provider", - "encrypted_file": { - "required_fields": ["provider", "envelope_path", "credential_reference", "expected_public_key"], - "provider_value": "encrypted_file" - } - } + "rotation_mode": "offline_create_new_configuration_apply_restart", + "live_rekey_route": false, + "live_replace_route": false }) ); + for removed in [ + "identity_rekey_request_v1", + "identity_replace_request_v1", + "identity_mutation_receipt_v1", + ] { + assert!(!models.contains_key(removed)); + } + for removed in [ + "credential_reference", + "encrypted_file_provider", + "identity_provider_replacement", + "encrypted_file_replacement", + ] { + assert!(!types.contains_key(removed)); + } assert_eq!( value["admin"]["types"]["service_phase"], serde_json::json!({ "kind": "enum", "values": ["starting", "ready", "degraded", "unready", "stopping", "failed"] }) @@ -291,7 +301,7 @@ fn admin_inventory_is_closed_unique_and_model_complete() { assert_eq!(mutation_operations, committed_effects); assert_eq!( decision_sections_digest(&value), - "cbb049e8aaa52db31ee99f18556fd6d9121032c06ac20377c5e63496092b539f" + "b227c6f248605a1672d3b7b07f60b0fd9ba8b890478d17e081cd7e3caed8a889" ); }