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:
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"
);
}