myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit eea96175b40fd85d9a2463aec44af243f6492ca6
parent d6bc2a722f72f403bd9ba2519401d8e0a874750c
Author: triesap <tyson@radroots.org>
Date:   Sun, 23 Aug 2026 11:40:04 +0000

admin: compose production control surfaces

- remove live identity rotation from final operator and CLI contracts
- bind the exact Myc handler to canonical permissioned Unix admin serving
- freeze status operations doctor and runtime ownership boundaries
- refresh API and regression evidence for the 19-route contract

Diffstat:
MAGENTS.md | 21+++++++++++++++++----
MREADME | 26++++++++++++++++++--------
Mcontracts/api_baselines/myc.txt | 46+++++++++++++++++++++++++++++++++++++++-------
Acontracts/services_hardening/control_surfaces.v1.json | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/services_hardening/operator_contract.v1.json | 35++++-------------------------------
Msrc/admin_v1.rs | 348+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/cli_v1.rs | 33---------------------------------
Msrc/delivery_worker.rs | 2+-
Msrc/lib.rs | 9+++++----
Msrc/provider_executor.rs | 2+-
Msrc/transport_nostr_adapter.rs | 2+-
Mtests/package_boundary.rs | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mtests/services_hardening_cli.rs | 38+++-----------------------------------
Mtests/services_hardening_contracts.rs | 23++++++++++-------------
Atests/services_hardening_control_surfaces.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 594 insertions(+), 196 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -102,8 +102,10 @@ code may commit a completion only through the atomic exact-response method; a retained completion without its immutable response and initial delivery state is inconsistent evidence and must never be repaired implicitly. -- Step 150 implements the exact 21-route and 35-model Myc Unix-admin adapter. - Keep the shared Lib router private; reject model drift, unknown/duplicate/null +- Step 150 introduced the historical 21-route and 35-model Myc Unix-admin + adapter. Step 159 unit 13 owns its final 19-route and 32-model form by + removing live identity rekey and replacement. Keep the shared Lib router + private; reject model drift, unknown/duplicate/null fields, noncanonical response bytes, invalid path/query values, and unsafe errors at the adapter boundary. Domain handlers must bind authenticated cursors to route/filter/snapshot identity and must retain exact operation-ID @@ -113,7 +115,9 @@ must select exactly daemon, offline, or permissioned Unix-admin authority; only the explicitly contracted read-only status, backup, and public-identity operations may fall back after proving the daemon writer lock is free. - Rekey, replace, and every other live mutation have no direct-state fallback. + Identity rekey and replacement are offline create-new/config-apply + operations and have no live command or route. Every retained live mutation + has no direct-state fallback. Do not reparse process arguments or let a live CLI plan obtain SQLite, provider, relay, task, signal, or runtime authority. - Step 153 freezes one ordered 13-check doctor engine. Check adapters retain @@ -159,8 +163,17 @@ acknowledgement to UnknownAcknowledgement, and retries only the exact committed signed bytes. Never hold a SQLite transaction across provider or relay work, detach protected blocking work, expose the executor/client, or - create one task per relay. Unit 13 alone wires these components into the + create one task per relay. Unit 15 alone wires these components into the fixed runtime graph and startup handshake. +- Step 159 unit 13 owns the final 19-route/32-model production Unix-admin + server around the exact handler boundary, the configured transport-limit + projection, the canonical permissioned `admin.sock` binding, and the + machine-bound composition facts for the existing sole status publisher, + passive three-route operations server, and injected 13-check doctor. Keep + shared routers, listeners, entropy, and cancellation private. Unit 14 owns + secure CLI/config bootstrap and concrete doctor probes; Unit 15 alone owns + server task spawning, provider/relay wiring, readiness, reconnect, and + shutdown. - Treat checked-in source, tests, and prototype behavior as implementation evidence, not permission to preserve behavior that the active requirement removes. diff --git a/README b/README @@ -60,7 +60,9 @@ requires explicit `--profile <service-host|interactive|repo-local>` and `--repo-local-root` only for `repo-local`, and accepts only an optional absolute `--config` path. Its exact command inventory is `run`; `config init|validate|show|schema`; `state init|status|backup|restore|verify|migrate`; -`identity init|status|rekey|replace|export-public`; `status`; and `doctor`. +`identity init|status|export-public`; `status`; and `doctor`. Identity rotation +is an offline create-new/config-apply lifecycle and is intentionally absent +from the live CLI and Unix-admin inventories. The process binary uses only this parser. Hardened command execution remains fail-closed until its ordered runtime-dispatch steps are complete; no prototype command is used as a fallback. @@ -70,8 +72,8 @@ selects daemon authority; config, exclusive state/identity provisioning, and doctor select offline authority; state/identity/service inspection and live mutations select the permissioned Unix-admin authority. Only explicitly read-only status, backup, and public-identity operations may fall back when a -later executor proves the daemon writer lock is free. Rekey and replace never -fall back to direct state access, and no live plan carries SQLite authority. +later executor proves the daemon writer lock is free. No live plan carries +SQLite authority. The active doctor boundary executes the exact 13-check operator inventory in contract order under fixed per-check deadlines. Check implementations retain @@ -235,8 +237,8 @@ authority. Explicit desired/current offline NIP-05 export re-reads verified state, works through read-only inspection, and returns canonical compact `names` plus NIP-46 discovery JSON without hosting it. -The implemented v1 Myc administration adapter registers the exact 21-route -inventory and validates every request and response against all 35 model +The implemented v1 Myc administration adapter registers the exact 19-route +inventory and validates every request and response against all 32 model references in `contracts/services_hardening/operator_contract.v1.json`. It reuses the hardened Lib HTTP/1.1-over-Unix server for original-wire duplicate and null rejection, percent-decoded bounded path parameters, peer admission, @@ -251,9 +253,17 @@ operation ID. A successful mutation means its contract-defined local effect and operation audit are committed; it does not claim relay submission or delivery. Identical operation-ID reuse must return the original committed result, different-byte reuse returns `operation_id_conflict`, and pagination -cursors remain authenticated to the same route, filters, and snapshot. This -checkpoint supplies the typed protocol adapter, not the later daemon runtime, -cached status/metrics producer, CLI dispatch, or TCP operations surface. +cursors remain authenticated to the same route, filters, and snapshot. Unit 13 +seals that handler boundary inside the production `MycAdminServer`, +projects the exact admitted admin transport limits, and binds only the +canonical permissioned `admin.sock` through the shared host authority. The raw +router, listener, entropy source, and cancellation token remain private. The +existing sole status publisher feeds both detailed local status and the +optional passive three-route TCP operations server, while doctor continues to +accept only its exact injected 13-check probe inventory. Unit 14 owns secure +CLI/config bootstrap and the concrete doctor probe implementations; Unit 15 +alone owns task spawning, provider/relay wiring, readiness, reconnect, and +phase-aware shutdown. The transport capability and admitted request remain non-forgeable outside the crate: diff --git a/contracts/api_baselines/myc.txt b/contracts/api_baselines/myc.txt @@ -60,21 +60,30 @@ pub myc::MycAdminRoute::DiscoveryRefresh pub myc::MycAdminRoute::DiscoveryRender pub myc::MycAdminRoute::EffectiveConfig pub myc::MycAdminRoute::IdentityPublic -pub myc::MycAdminRoute::IdentityRekey -pub myc::MycAdminRoute::IdentityReplace pub myc::MycAdminRoute::IdentityStatus pub myc::MycAdminRoute::MetricsSnapshot pub myc::MycAdminRoute::StateBackup pub myc::MycAdminRoute::StateStatus pub myc::MycAdminRoute::Status impl myc::MycAdminRoute -pub const myc::MycAdminRoute::ALL: [Self; 21] +pub const myc::MycAdminRoute::ALL: [Self; 19] pub const fn myc::MycAdminRoute::is_mutation(self) -> bool pub const fn myc::MycAdminRoute::method(self) -> myc::MycAdminMethod pub const fn myc::MycAdminRoute::operation_id(self) -> &'static str pub const fn myc::MycAdminRoute::path(self) -> &'static str pub const fn myc::MycAdminRoute::request_model(self) -> &'static str pub const fn myc::MycAdminRoute::response_model(self) -> &'static str +pub enum myc::MycAdminServerErrorKind +pub myc::MycAdminServerErrorKind::Accept +pub myc::MycAdminServerErrorKind::Bind +pub myc::MycAdminServerErrorKind::ConnectionTaskPanicked +pub myc::MycAdminServerErrorKind::InvalidConfiguration +pub myc::MycAdminServerErrorKind::Listener +pub myc::MycAdminServerErrorKind::Router +pub myc::MycAdminServerErrorKind::ServerConfiguration +pub myc::MycAdminServerErrorKind::WriterAuthority +impl myc::MycAdminServerErrorKind +pub const fn myc::MycAdminServerErrorKind::code(self) -> &'static str pub enum myc::MycAuditKind pub myc::MycAuditKind::ChallengeAuthorization pub myc::MycAuditKind::ChallengeCreation @@ -124,8 +133,6 @@ pub myc::MycBootstrapProfileV1::RepoLocal pub myc::MycBootstrapProfileV1::ServiceHost pub enum myc::MycCliAdminOperationV1 pub myc::MycCliAdminOperationV1::IdentityPublic -pub myc::MycCliAdminOperationV1::IdentityRekey -pub myc::MycCliAdminOperationV1::IdentityReplace pub myc::MycCliAdminOperationV1::IdentityStatus pub myc::MycCliAdminOperationV1::StateBackup pub myc::MycCliAdminOperationV1::StateStatus @@ -404,8 +411,6 @@ pub const fn myc::MycGovernanceStateErrorKind::code(self) -> &'static str pub enum myc::MycIdentityCommandV1 pub myc::MycIdentityCommandV1::ExportPublic pub myc::MycIdentityCommandV1::Init -pub myc::MycIdentityCommandV1::Rekey -pub myc::MycIdentityCommandV1::Replace pub myc::MycIdentityCommandV1::Status pub enum myc::MycIntegrityStateV1 pub myc::MycIntegrityStateV1::Failed @@ -804,6 +809,13 @@ pub enum myc::MycTransportHealthV1 pub myc::MycTransportHealthV1::Degraded pub myc::MycTransportHealthV1::Ready pub myc::MycTransportHealthV1::Unavailable +pub struct myc::MycAdminCancellationToken +impl myc::MycAdminCancellationToken +pub fn myc::MycAdminCancellationToken::cancel(&self) +pub fn myc::MycAdminCancellationToken::is_cancelled(&self) -> bool +pub fn myc::MycAdminCancellationToken::new() -> Self +impl core::fmt::Debug for myc::MycAdminCancellationToken +pub fn myc::MycAdminCancellationToken::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycAdminDocumentError impl myc::MycAdminDocumentError pub const fn myc::MycAdminDocumentError::kind(self) -> myc::MycAdminDocumentErrorKind @@ -858,6 +870,21 @@ pub struct myc::MycAdminRouterError impl core::error::Error for myc::MycAdminRouterError impl core::fmt::Display for myc::MycAdminRouterError pub fn myc::MycAdminRouterError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycAdminServer +impl myc::MycAdminServer +pub async fn myc::MycAdminServer::bind(self, &myc::MycRuntimeContext) -> core::result::Result<myc::MycBoundAdminServer, myc::MycAdminServerError> +pub fn myc::MycAdminServer::new<H>(&myc::MycConfigDocumentV1, alloc::sync::Arc<H>) -> core::result::Result<Self, myc::MycAdminServerError> where H: myc::MycAdminHandler +impl core::fmt::Debug for myc::MycAdminServer +pub fn myc::MycAdminServer::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycAdminServerError +impl myc::MycAdminServerError +pub const fn myc::MycAdminServerError::code(self) -> &'static str +pub const fn myc::MycAdminServerError::kind(self) -> myc::MycAdminServerErrorKind +impl core::error::Error for myc::MycAdminServerError +impl core::fmt::Debug for myc::MycAdminServerError +pub fn myc::MycAdminServerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for myc::MycAdminServerError +pub fn myc::MycAdminServerError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycAuditCorrelationId(_) impl myc::MycAuditCorrelationId pub const fn myc::MycAuditCorrelationId::new([u8; 32]) -> Self @@ -918,6 +945,11 @@ pub fn myc::MycAuthorizationChallengeUrl::as_str(&self) -> &str pub fn myc::MycAuthorizationChallengeUrl::new(&str) -> core::result::Result<Self, myc::MycConnectionStateError> impl core::fmt::Debug for myc::MycAuthorizationChallengeUrl pub fn myc::MycAuthorizationChallengeUrl::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycBoundAdminServer +impl myc::MycBoundAdminServer +pub async fn myc::MycBoundAdminServer::serve(self, myc::MycAdminCancellationToken) -> core::result::Result<(), myc::MycAdminServerError> +impl core::fmt::Debug for myc::MycBoundAdminServer +pub fn myc::MycBoundAdminServer::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycBoundOperationsServer impl myc::MycBoundOperationsServer pub fn myc::MycBoundOperationsServer::local_address(&self) -> core::net::socket_addr::SocketAddr diff --git a/contracts/services_hardening/control_surfaces.v1.json b/contracts/services_hardening/control_surfaces.v1.json @@ -0,0 +1,56 @@ +{ + "schema": "radroots.myc.control-surfaces.v1", + "contract_version": 1, + "step": 159, + "unit": 13, + "unit_id": "myc-control-surfaces", + "admin": { + "transport": "http_1_1_over_permissioned_unix_domain_socket", + "route_count": 19, + "model_count": 32, + "handler_boundary": "MycAdminHandler", + "server": "MycAdminServer", + "bound_server": "MycBoundAdminServer", + "cancellation": "MycAdminCancellationToken", + "limits_source": "/resource_limits/admin", + "system_entropy": true, + "canonical_runtime_directory": true, + "canonical_socket_artifact": "admin.sock", + "raw_router_or_listener_exposed": false, + "live_identity_mutation_routes": false, + "identity_rotation": "offline_create_new_then_config_apply" + }, + "status": { + "factory": "myc_status_cache", + "publisher": "MycStatusPublisher", + "publisher_count": 1, + "passive_reader": "MycStatusReader", + "fresh_probe_on_read": false + }, + "operations": { + "server": "MycOperationsServer", + "enabled_only_by_validated_configuration": true, + "routes": ["/livez", "/readyz", "/metrics"], + "reads_same_status_cache": true, + "active_probe": false + }, + "doctor": { + "runner": "run_myc_doctor", + "check_count": 13, + "probe_authority_injected": true, + "used_as_liveness_or_readiness_probe": false + }, + "security": { + "source_free_public_errors": true, + "paths_in_display_or_debug": false, + "raw_sqlx_or_transport_types_public": false, + "tcp_admin_routes": false + }, + "deferred": { + "secure_cli_and_config_bootstrap": "unit_14", + "production_doctor_probe_implementations": "unit_14", + "authoritative_daemon_task_graph": "unit_15", + "provider_relay_runtime_wiring": "unit_15", + "readiness_reconnect_and_shutdown": "unit_15" + } +} 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.myc.status.get.v1", "request_model": "empty", "response_model": "service_status_v1", "mutation": false }, { "method": "GET", "path": "/v1/config/effective", "operation_id": "radroots.myc.config.effective.get.v1", "request_model": "empty", "response_model": "effective_config_v1", "mutation": false }, { "method": "GET", "path": "/v1/identity/status", "operation_id": "radroots.myc.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.myc.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.myc.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.myc.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.myc.state.status.get.v1", "request_model": "empty", "response_model": "state_status_v1", "mutation": false }, { "method": "POST", "path": "/v1/state/backup", "operation_id": "radroots.myc.state.backup.create.v1", "request_model": "state_backup_request_v1", "response_model": "state_backup_receipt_v1", "mutation": true }, @@ -54,18 +52,9 @@ { "id": "user", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, { "id": "discovery", "required": false, "disabled_allowed": true, "providers": ["encrypted_file", "local_signer"] } ], - "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" - }, - "local_signer": { - "required_fields": ["provider", "socket_path", "request_deadline_ms", "request_max_bytes", "response_max_bytes", "concurrency", "expected_public_key"], - "provider_value": "local_signer" - } - } + "live_mutation": false, + "rotation_mode": "offline_create_new_then_config_apply", + "in_place_overwrite": false }, "types": { "myc_literal": { "kind": "literal", "value": "myc" }, @@ -82,18 +71,9 @@ "sha256_hex": { "kind": "string", "utf8_bytes": 64, "pattern": "^[0-9a-f]{64}$" }, "public_key_hex": { "kind": "string", "utf8_bytes": 64, "pattern": "^[0-9a-f]{64}$" }, "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": ["transport", "user", "discovery"] }, - "encrypted_file_provider": { "kind": "literal", "value": "encrypted_file" }, "identity_provider_kind": { "kind": "enum", "values": ["encrypted_file", "local_signer"] }, - "identity_provider_replacement": { "kind": "tagged_union", "discriminator": "provider", "variants": ["encrypted_file_replacement", "local_signer_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" } }, - "local_signer_replacement": { "kind": "closed_object", "fields": { "provider": "local_signer_provider", "socket_path": "absolute_path", "request_deadline_ms": "deadline_ms", "request_max_bytes": "bounded_io_bytes", "response_max_bytes": "bounded_io_bytes", "concurrency": "bounded_concurrency", "expected_public_key": "public_key_hex" } }, - "local_signer_provider": { "kind": "literal", "value": "local_signer" }, - "deadline_ms": { "kind": "integer", "minimum": 1, "maximum": 30000, "unit": "milliseconds" }, - "bounded_io_bytes": { "kind": "integer", "minimum": 1024, "maximum": 1048576, "unit": "bytes" }, - "bounded_concurrency": { "kind": "integer", "minimum": 1, "maximum": 64 }, "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"] }, @@ -155,7 +135,7 @@ "optional_utc_seconds": { "kind": "optional", "representation": "absent_parent_field", "value": "utc_seconds" }, "connection_summaries": { "kind": "array", "items": "connection_summary", "maximum_items": 200, "ordered": true }, "connection_summary": { "kind": "closed_object", "fields": { "connection_id": "bounded_id", "client_public_key": "public_key_hex", "state": "connection_state", "permissions": "permission_set", "generation": "u64", "created_at_utc": "utc_seconds", "updated_at_utc": "utc_seconds" } }, - "request_identity": { "kind": "string", "minimum_utf8_bytes": 1, "maximum_utf8_bytes": 256 }, + "request_identity": { "kind": "alias", "target": "sha256_hex" }, "audit_kind": { "kind": "string", "minimum_utf8_bytes": 1, "maximum_utf8_bytes": 64, "pattern": "^[a-z][a-z0-9_]*$" }, "audit_events": { "kind": "array", "items": "audit_event", "maximum_items": 200, "ordered": true }, "audit_event": { "kind": "closed_object", "fields": { "audit_id": "bounded_id", "occurred_at_utc": "utc_seconds", "kind": "audit_kind", "outcome": "audit_outcome", "reason_code": "reason_code", "correlation_id": "bounded_id" } }, @@ -172,9 +152,6 @@ "effective_config_v1": { "fields": { "schema": { "type": "myc_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" } } }, "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": "optional" }, "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" } } }, @@ -228,8 +205,6 @@ "success_visibility": "response_only_after_local_authoritative_commit", "relay_submission_or_delivery_required_for_success": false, "committed_effects": { - "radroots.myc.identity.rekey.v1": "identity_generation_provider_binding_session_invalidation_and_operation_audit", - "radroots.myc.identity.replace.v1": "identity_generation_provider_binding_session_invalidation_and_operation_audit", "radroots.myc.state.backup.create.v1": "verified_snapshot_manifest_digest_and_operation_audit", "radroots.myc.connection.approve.v1": "connection_state_permissions_generation_and_operation_audit", "radroots.myc.connection.reject.v1": "connection_state_reason_generation_and_operation_audit", @@ -262,8 +237,6 @@ { "command": "state migrate", "primary_authority": "offline", "offline_operation": "state_exclusive" }, { "command": "identity init", "primary_authority": "offline", "offline_operation": "identity_exclusive" }, { "command": "identity status", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/status", "offline_operation": "identity_read_only", "daemon_unavailable_offline_fallback": true }, - { "command": "identity rekey", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/rekey" }, - { "command": "identity replace", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/replace" }, { "command": "identity export-public", "primary_authority": "live_unix_admin", "admin_route": "/v1/identity/public", "offline_operation": "identity_read_only", "daemon_unavailable_offline_fallback": true }, { "command": "status", "primary_authority": "live_unix_admin", "admin_route": "/v1/status", "offline_operation": "state_read_only", "daemon_unavailable_offline_fallback": true }, { "command": "doctor", "primary_authority": "offline", "offline_operation": "doctor" } diff --git a/src/admin_v1.rs b/src/admin_v1.rs @@ -1,6 +1,6 @@ //! Exact Myc v1 Unix-admin route and model boundary. -use core::{fmt, future::Future, pin::Pin}; +use core::{fmt, future::Future, pin::Pin, time::Duration}; use std::{ collections::BTreeSet, error::Error, @@ -12,7 +12,9 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use radroots_service_host::{ AdminCorrelationId, AdminError, AdminErrorCode, AdminErrorMessage, AdminHttpMethod, AdminMutationRequest, AdminOperationId, AdminRequest, AdminRouteFailure, - AdminRouteFailureStatus, AdminRouteOutcome, AdminRouter, + AdminRouteFailureStatus, AdminRouteOutcome, AdminRouter, AdminServer, AdminServerError, + AdminTransportLimitValues, AdminTransportLimits, CancellationToken, UnixAdminSocketBinding, + UnixAdminSocketWriterAuthority, }; use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; use serde_json::{Map, Value}; @@ -37,8 +39,6 @@ pub enum MycAdminRoute { Status, EffectiveConfig, IdentityStatus, - IdentityRekey, - IdentityReplace, IdentityPublic, StateStatus, StateBackup, @@ -58,12 +58,10 @@ pub enum MycAdminRoute { } impl MycAdminRoute { - pub const ALL: [Self; 21] = [ + pub const ALL: [Self; 19] = [ Self::Status, Self::EffectiveConfig, Self::IdentityStatus, - Self::IdentityRekey, - Self::IdentityReplace, Self::IdentityPublic, Self::StateStatus, Self::StateBackup, @@ -95,9 +93,7 @@ impl MycAdminRoute { | Self::AuditEvents | Self::AuditSummary | Self::DiscoveryDesired => MycAdminMethod::Get, - Self::IdentityRekey - | Self::IdentityReplace - | Self::StateBackup + Self::StateBackup | Self::ConnectionApprove | Self::ConnectionReject | Self::ConnectionRevoke @@ -115,8 +111,6 @@ impl MycAdminRoute { 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", @@ -142,8 +136,6 @@ impl MycAdminRoute { Self::Status => "radroots.myc.status.get.v1", Self::EffectiveConfig => "radroots.myc.config.effective.get.v1", Self::IdentityStatus => "radroots.myc.identity.status.get.v1", - Self::IdentityRekey => "radroots.myc.identity.rekey.v1", - Self::IdentityReplace => "radroots.myc.identity.replace.v1", Self::IdentityPublic => "radroots.myc.identity.public.get.v1", Self::StateStatus => "radroots.myc.state.status.get.v1", Self::StateBackup => "radroots.myc.state.backup.create.v1", @@ -172,8 +164,6 @@ impl MycAdminRoute { | Self::MetricsSnapshot | Self::DiscoveryDesired => "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::ConnectionsList => "connections_query_v1", @@ -196,7 +186,6 @@ impl MycAdminRoute { 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", @@ -499,6 +488,189 @@ impl fmt::Debug for MycAdminRouter { } } +impl MycAdminRouter { + fn into_inner(self) -> AdminRouter { + self.inner + } +} + +/// Cloneable cooperative cancellation for the Myc Unix-admin server. +#[derive(Clone, Default)] +pub struct MycAdminCancellationToken { + inner: CancellationToken, +} + +impl MycAdminCancellationToken { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Requests cancellation. Repeated requests have no additional effect. + pub fn cancel(&self) { + self.inner.cancel(); + } + + #[must_use] + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } +} + +impl fmt::Debug for MycAdminCancellationToken { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycAdminCancellationToken") + .field("cancelled", &self.is_cancelled()) + .finish() + } +} + +/// Stable source-free Myc Unix-admin server failure classification. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycAdminServerErrorKind { + InvalidConfiguration, + Router, + ServerConfiguration, + WriterAuthority, + Bind, + Listener, + Accept, + ConnectionTaskPanicked, +} + +impl MycAdminServerErrorKind { + #[must_use] + pub const fn code(self) -> &'static str { + match self { + Self::InvalidConfiguration => "admin_configuration_invalid", + Self::Router => "admin_router_invalid", + Self::ServerConfiguration => "admin_server_configuration_invalid", + Self::WriterAuthority => "admin_writer_authority_unavailable", + Self::Bind => "admin_bind_failed", + Self::Listener => "admin_listener_failed", + Self::Accept => "admin_accept_failed", + Self::ConnectionTaskPanicked => "admin_connection_task_panicked", + } + } +} + +/// One redacted source-free Myc Unix-admin server failure. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MycAdminServerError { + kind: MycAdminServerErrorKind, +} + +impl MycAdminServerError { + const fn new(kind: MycAdminServerErrorKind) -> Self { + Self { kind } + } + + #[must_use] + pub const fn kind(self) -> MycAdminServerErrorKind { + self.kind + } + + #[must_use] + pub const fn code(self) -> &'static str { + self.kind.code() + } +} + +impl fmt::Debug for MycAdminServerError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycAdminServerError") + .field("kind", &self.kind) + .finish() + } +} + +impl fmt::Display for MycAdminServerError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Myc Unix-admin server failed") + } +} + +impl Error for MycAdminServerError {} + +/// Unbound production Myc Unix-admin server. +/// +/// Construction projects only the already-admitted Myc 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 MycAdminServer { + inner: AdminServer, +} + +impl MycAdminServer { + pub fn new<H>( + configuration: &crate::MycConfigDocumentV1, + handler: Arc<H>, + ) -> Result<Self, MycAdminServerError> + where + H: MycAdminHandler, + { + let limits = admin_transport_limits(configuration)?; + let router = build_myc_admin_router(handler) + .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::Router))?; + let inner = AdminServer::with_system_entropy(router.into_inner(), limits) + .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::ServerConfiguration))?; + Ok(Self { inner }) + } + + /// Acquires the canonical runtime-directory authority and binds `admin.sock`. + /// + /// Binding does not spawn a task or begin request admission. Unit 15 owns + /// the final supervised server task and its shutdown phase. + pub async fn bind( + self, + runtime: &crate::MycRuntimeContext, + ) -> Result<MycBoundAdminServer, MycAdminServerError> { + let authority = UnixAdminSocketWriterAuthority::acquire(runtime.context().paths().run()) + .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::WriterAuthority))?; + let binding = UnixAdminSocketBinding::bind(authority, runtime.artifacts().admin_socket()) + .await + .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::Bind))?; + Ok(MycBoundAdminServer { + inner: self.inner, + binding, + }) + } +} + +impl fmt::Debug for MycAdminServer { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("MycAdminServer([sealed])") + } +} + +/// Bound production Myc Unix-admin server. +pub struct MycBoundAdminServer { + inner: AdminServer, + binding: UnixAdminSocketBinding, +} + +impl MycBoundAdminServer { + /// Serves until supervisor cancellation and then drains bounded connection work. + pub async fn serve( + self, + cancellation: MycAdminCancellationToken, + ) -> Result<(), MycAdminServerError> { + self.inner + .serve(self.binding, cancellation.inner) + .await + .map_err(map_admin_server_error) + } +} + +impl fmt::Debug for MycBoundAdminServer { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("MycBoundAdminServer([sealed])") + } +} + /// Registers the complete closed Myc v1 route inventory on the hardened Lib router. pub fn build_myc_admin_router<H>(handler: Arc<H>) -> Result<MycAdminRouter, MycAdminRouterError> where @@ -520,6 +692,52 @@ where Ok(MycAdminRouter { inner: router }) } +fn admin_transport_limits( + configuration: &crate::MycConfigDocumentV1, +) -> Result<AdminTransportLimits, MycAdminServerError> { + let admin = configuration + .normalized() + .pointer("/resource_limits/admin") + .ok_or_else(invalid_admin_configuration)?; + let values = AdminTransportLimitValues { + header_count: admin_u32(admin, "/header_count")?, + header_bytes: admin_u32(admin, "/header_bytes")?, + request_body_utf8_bytes: admin_u32(admin, "/request_body_utf8_bytes")?, + response_body_utf8_bytes: admin_u32(admin, "/response_body_utf8_bytes")?, + concurrent_connections: admin_u32(admin, "/concurrent_connections")?, + request_deadline: Duration::from_millis(admin_u64(admin, "/request_deadline_ms")?), + idle_timeout: Duration::from_millis(admin_u64(admin, "/idle_timeout_ms")?), + query_items: admin_u32(admin, "/query_items")?, + }; + AdminTransportLimits::new(values).map_err(|_| invalid_admin_configuration()) +} + +fn admin_u64(value: &Value, pointer: &str) -> Result<u64, MycAdminServerError> { + value + .pointer(pointer) + .and_then(Value::as_u64) + .ok_or_else(invalid_admin_configuration) +} + +fn admin_u32(value: &Value, pointer: &str) -> Result<u32, MycAdminServerError> { + u32::try_from(admin_u64(value, pointer)?).map_err(|_| invalid_admin_configuration()) +} + +const fn invalid_admin_configuration() -> MycAdminServerError { + MycAdminServerError::new(MycAdminServerErrorKind::InvalidConfiguration) +} + +const fn map_admin_server_error(error: AdminServerError) -> MycAdminServerError { + let kind = match error { + AdminServerError::ListenerClone { .. } | AdminServerError::ListenerRegistration { .. } => { + MycAdminServerErrorKind::Listener + } + AdminServerError::Accept { .. } => MycAdminServerErrorKind::Accept, + AdminServerError::ConnectionTaskPanicked => MycAdminServerErrorKind::ConnectionTaskPanicked, + }; + MycAdminServerError::new(kind) +} + async fn dispatch_route<H>( route: MycAdminRoute, handler: Arc<H>, @@ -773,7 +991,7 @@ fn operator_route_inventory_is_exact() -> bool { return false; }; routes.len() == MycAdminRoute::ALL.len() - && models.len() == 35 + && models.len() == 32 && admin .pointer("/model_wire_contract/response_body_max_utf8_bytes") .and_then(Value::as_u64) @@ -1446,7 +1664,7 @@ mod tests { #[test] fn complete_route_and_model_inventory_matches_the_machine_contract() { assert!(operator_route_inventory_is_exact()); - assert_eq!(MycAdminRoute::ALL.len(), 21); + assert_eq!(MycAdminRoute::ALL.len(), 19); let referenced = MycAdminRoute::ALL .into_iter() .flat_map(|route| [route.request_model(), route.response_model()]) @@ -1458,7 +1676,7 @@ mod tests { .map(String::as_str) .collect::<BTreeSet<_>>(); assert_eq!(referenced, governed); - assert_eq!(governed.len(), 35); + assert_eq!(governed.len(), 32); for model in governed { let value = sample_model(model); validate_model(model, &value).expect("minimum exact model"); @@ -1573,41 +1791,36 @@ mod tests { fn public_diagnostics_are_source_free_and_content_free() { let document = MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel); let handler = MycAdminHandlerError::new(MycAdminHandlerErrorKind::Internal); + let server = MycAdminServerError::new(MycAdminServerErrorKind::Bind); for rendered in [ format!("{document}"), format!("{document:?}"), format!("{handler}"), format!("{handler:?}"), + format!("{server}"), + format!("{server:?}"), ] { assert!(!rendered.contains("/tmp/protected")); assert!(!rendered.contains("credential")); } assert!(Error::source(&document).is_none()); assert!(Error::source(&handler).is_none()); + assert!(Error::source(&server).is_none()); + assert_eq!(server.code(), "admin_bind_failed"); } #[cfg(any(target_os = "linux", target_os = "macos"))] mod native { use core::sync::atomic::{AtomicUsize, Ordering}; + use std::fs; use std::sync::Mutex; - use radroots_service_host::{ - AdminClient, AdminClientTarget, AdminServer, AdminTransportLimits, CancellationToken, - EntropyError, EntropySource, UnixAdminSocketBinding, UnixAdminSocketWriterAuthority, - }; + use radroots_service_host::{AdminClient, AdminClientTarget, AdminTransportLimits}; use super::*; use tokio::io::{AsyncReadExt, AsyncWriteExt}; - #[derive(Clone, Copy)] - struct FixedEntropy; - - impl EntropySource for FixedEntropy { - fn fill_bytes(&self, destination: &mut [u8]) -> Result<(), EntropyError> { - destination.fill(0x51); - Ok(()) - } - } + const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); type FixtureCall = (MycAdminRoute, Option<String>, Box<[u8]>); @@ -1660,11 +1873,38 @@ mod tests { } } - fn runtime_directory() -> tempfile::TempDir { - tempfile::Builder::new() + fn runtime_context() -> ( + tempfile::TempDir, + crate::MycRuntimeContext, + crate::MycConfigDocumentV1, + ) { + let root = tempfile::Builder::new() .prefix("myc-admin-") .tempdir_in("/tmp") - .expect("short runtime directory") + .expect("short runtime root"); + let root_path = root.path().to_str().expect("UTF-8 test root"); + let invocation = crate::parse_myc_cli_v1_from([ + "myc", + "--profile", + "repo-local", + "--instance", + "primary", + "--repo-local-root", + root_path, + "run", + ]) + .expect("test CLI"); + let resolver = crate::RadrootsPathResolver::new( + crate::RadrootsPlatform::Linux, + crate::RadrootsHostEnvironment::default(), + ); + let runtime = crate::resolve_myc_runtime_context(&resolver, &invocation) + .expect("test runtime context"); + fs::create_dir_all(runtime.context().paths().run()).expect("runtime directory"); + let configuration = + crate::parse_myc_config_v1(CONFIG.as_bytes(), crate::MycConfigProfile::RepoLocal) + .expect("test configuration"); + (root, runtime, configuration) } fn target_for(route: MycAdminRoute, request: &Value) -> AdminClientTarget { @@ -1709,24 +1949,20 @@ mod tests { } #[tokio::test] - async fn all_twenty_one_routes_round_trip_over_the_hardened_unix_boundary() { - let directory = runtime_directory(); - let socket = directory.path().join("admin.sock"); - let authority = UnixAdminSocketWriterAuthority::acquire(directory.path()) - .expect("writer authority"); - let binding = UnixAdminSocketBinding::bind(authority, &socket) - .await - .expect("socket binding"); + async fn all_nineteen_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 MycAdminRouter { inner } = - build_myc_admin_router(Arc::clone(&handler)).expect("exact router"); - let server = AdminServer::new(inner, AdminTransportLimits::DEFAULT, FixedEntropy) - .expect("admin server"); - let cancellation = CancellationToken::new(); + let server = MycAdminServer::new(&configuration, Arc::clone(&handler)) + .expect("production admin server") + .bind(&runtime) + .await + .expect("canonical admin binding"); + let cancellation = MycAdminCancellationToken::new(); let server_cancellation = cancellation.clone(); let task = tokio::spawn(async move { server - .serve(binding, server_cancellation) + .serve(server_cancellation) .await .expect("serve Myc admin"); }); @@ -1813,7 +2049,7 @@ mod tests { { let calls = handler.calls.lock().expect("calls"); - assert_eq!(calls.len(), 21); + assert_eq!(calls.len(), 19); for (index, (route, operation_id, request)) in calls.iter().enumerate() { assert_eq!(*route, MycAdminRoute::ALL[index]); assert_eq!(operation_id.is_some(), route.is_mutation()); @@ -1826,6 +2062,16 @@ mod tests { } cancellation.cancel(); task.await.expect("server task"); + assert!(!socket.exists()); + } + + #[test] + fn production_server_projects_exact_validated_admin_limits() { + let (_root, _runtime, configuration) = runtime_context(); + assert_eq!( + admin_transport_limits(&configuration).expect("admin limits"), + AdminTransportLimits::DEFAULT + ); } } } diff --git a/src/cli_v1.rs b/src/cli_v1.rs @@ -53,8 +53,6 @@ pub enum MycStateCommandV1 { pub enum MycIdentityCommandV1 { Init, Status, - Rekey, - Replace, ExportPublic, } @@ -84,8 +82,6 @@ pub enum MycCliAdminOperationV1 { StateStatus, StateBackup, IdentityStatus, - IdentityRekey, - IdentityReplace, IdentityPublic, } @@ -99,8 +95,6 @@ impl MycCliAdminOperationV1 { Self::StateStatus => crate::MycAdminRoute::StateStatus, Self::StateBackup => crate::MycAdminRoute::StateBackup, Self::IdentityStatus => crate::MycAdminRoute::IdentityStatus, - Self::IdentityRekey => crate::MycAdminRoute::IdentityRekey, - Self::IdentityReplace => crate::MycAdminRoute::IdentityReplace, Self::IdentityPublic => crate::MycAdminRoute::IdentityPublic, } } @@ -362,12 +356,6 @@ pub const fn plan_myc_cli_v1(invocation: &MycCliInvocationV1) -> MycCliExecution MycCliAdminOperationV1::IdentityPublic, MycCliOfflineOperationV1::IdentityReadOnly, ), - MycCommandV1::Identity(MycIdentityCommandV1::Rekey) => { - admin_plan(MycCliAdminOperationV1::IdentityRekey) - } - MycCommandV1::Identity(MycIdentityCommandV1::Replace) => { - admin_plan(MycCliAdminOperationV1::IdentityReplace) - } MycCommandV1::Status => read_only_admin_plan( MycCliAdminOperationV1::Status, MycCliOfflineOperationV1::StateReadOnly, @@ -394,15 +382,6 @@ const fn offline_plan(operation: MycCliOfflineOperationV1) -> MycCliExecutionPla } } -const fn admin_plan(operation: MycCliAdminOperationV1) -> MycCliExecutionPlanV1 { - MycCliExecutionPlanV1 { - primary_authority: MycCliPrimaryAuthorityV1::LiveUnixAdmin, - offline_operation: None, - admin_operation: Some(operation), - daemon_unavailable_offline_fallback: false, - } -} - const fn read_only_admin_plan( admin_operation: MycCliAdminOperationV1, offline_operation: MycCliOfflineOperationV1, @@ -557,8 +536,6 @@ impl From<RawStateCommand> for MycStateCommandV1 { enum RawIdentityCommand { Init, Status, - Rekey, - Replace, ExportPublic, } @@ -567,8 +544,6 @@ impl From<RawIdentityCommand> for MycIdentityCommandV1 { match value { RawIdentityCommand::Init => Self::Init, RawIdentityCommand::Status => Self::Status, - RawIdentityCommand::Rekey => Self::Rekey, - RawIdentityCommand::Replace => Self::Replace, RawIdentityCommand::ExportPublic => Self::ExportPublic, } } @@ -637,14 +612,6 @@ mod tests { MycCommandV1::Identity(MycIdentityCommandV1::Status), ), ( - &["identity", "rekey"][..], - MycCommandV1::Identity(MycIdentityCommandV1::Rekey), - ), - ( - &["identity", "replace"][..], - MycCommandV1::Identity(MycIdentityCommandV1::Replace), - ), - ( &["identity", "export-public"][..], MycCommandV1::Identity(MycIdentityCommandV1::ExportPublic), ), diff --git a/src/delivery_worker.rs b/src/delivery_worker.rs @@ -2,7 +2,7 @@ #![allow( dead_code, - reason = "Step 159 Unit 12 seals the worker before Unit 13 runtime graph wiring" + reason = "Step 159 Unit 12 seals the worker before Unit 15 runtime graph wiring" )] use core::fmt; diff --git a/src/lib.rs b/src/lib.rs @@ -50,10 +50,11 @@ mod transport_nostr_adapter; #[cfg(any(target_os = "linux", target_os = "macos"))] pub use admin_v1::{ - MycAdminDocumentError, MycAdminDocumentErrorKind, MycAdminFuture, MycAdminHandler, - MycAdminHandlerError, MycAdminHandlerErrorKind, MycAdminMethod, MycAdminRequestDocument, - MycAdminResponseDocument, MycAdminRoute, MycAdminRouter, MycAdminRouterError, - build_myc_admin_router, + MycAdminCancellationToken, MycAdminDocumentError, MycAdminDocumentErrorKind, MycAdminFuture, + MycAdminHandler, MycAdminHandlerError, MycAdminHandlerErrorKind, MycAdminMethod, + MycAdminRequestDocument, MycAdminResponseDocument, MycAdminRoute, MycAdminRouter, + MycAdminRouterError, MycAdminServer, MycAdminServerError, MycAdminServerErrorKind, + MycBoundAdminServer, build_myc_admin_router, }; pub use cli_v1::{ MycBootstrapProfileV1, MycCliAdminOperationV1, MycCliExecutionPlanV1, MycCliInvocationV1, diff --git a/src/provider_executor.rs b/src/provider_executor.rs @@ -2,7 +2,7 @@ #![allow( dead_code, - reason = "Step 159 Unit 12 seals the executor before Unit 13 runtime graph wiring" + reason = "Step 159 Unit 12 seals the executor before Unit 15 runtime graph wiring" )] use core::fmt; diff --git a/src/transport_nostr_adapter.rs b/src/transport_nostr_adapter.rs @@ -2,7 +2,7 @@ #![allow( dead_code, - reason = "Step 159 Unit 12 seals the adapter before Unit 13 runtime graph wiring" + reason = "Step 159 Unit 12 seals the adapter before Unit 15 runtime graph wiring" )] use core::{fmt, future::Future, pin::Pin}; diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -6,6 +6,8 @@ const ROOT: &str = include_str!("../src/lib.rs"); const README: &str = include_str!("../README"); const PUBLIC_API: &str = include_str!("../contracts/api_baselines/myc.txt"); const ADMIN_V1: &str = include_str!("../src/admin_v1.rs"); +const CONTROL_SURFACES_CONTRACT: &str = + include_str!("../contracts/services_hardening/control_surfaces.v1.json"); const NIP46_VERIFICATION: &str = include_str!("../src/nip46_verification.rs"); const NIP46_AUTHORIZATION: &str = include_str!("../src/nip46_authorization.rs"); const NIP46_REPLAY: &str = include_str!("../src/nip46_replay.rs"); @@ -211,6 +213,11 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() { "pub trait myc::MycAdminHandler", "pub struct myc::MycAdminRouter", "pub fn myc::build_myc_admin_router", + "pub struct myc::MycAdminServer", + "pub struct myc::MycBoundAdminServer", + "pub struct myc::MycAdminCancellationToken", + "pub struct myc::MycAdminServerError", + "pub enum myc::MycAdminServerErrorKind", "pub struct myc::MycRuntimeContext", "pub struct myc::MycRuntimeFoundation", "pub struct myc::MycRuntimeReadiness", @@ -885,8 +892,8 @@ fn step150_admin_adapter_is_closed_typed_and_transport_bounded() { "#[cfg(any(target_os = \"linux\", target_os = \"macos\"))]\npub use admin_v1::{" )); for required in [ - "pub const ALL: [Self; 21]", - "models.len() == 35", + "pub const ALL: [Self; 19]", + "models.len() == 32", "operator_route_inventory_is_exact", "AdminMutationRequest<Value>", "strict_json(bytes)", @@ -897,7 +904,7 @@ fn step150_admin_adapter_is_closed_typed_and_transport_bounded() { "original committed response", "same route, filters, and snapshot", "relay submission or delivery is not implied", - "all_twenty_one_routes_round_trip_over_the_hardened_unix_boundary", + "all_nineteen_routes_round_trip_over_the_hardened_unix_boundary", ] { assert!( ADMIN_V1.contains(required), @@ -923,17 +930,62 @@ fn step150_admin_adapter_is_closed_typed_and_transport_bounded() { ); } for required in [ - "exact 21-route", - "all 35 model", + "exact 19-route", + "all 32 model", "Raw shared-host routers and JSON values never cross the public", "operation_id_conflict", - "not the later daemon runtime", + "Unit 13\nseals that handler boundary inside the production `MycAdminServer`", + "Unit 15\nalone owns task spawning, provider/relay wiring, readiness, reconnect, and\nphase-aware shutdown", ] { assert!(README.contains(required), "README is missing `{required}`"); } } #[test] +fn step159_unit13_control_surfaces_are_sealed_and_machine_bound() { + let contract: serde_json::Value = + serde_json::from_str(CONTROL_SURFACES_CONTRACT).expect("control-surface contract"); + assert_eq!(contract["schema"], "radroots.myc.control-surfaces.v1"); + assert_eq!(contract["step"], 159); + assert_eq!(contract["unit"], 13); + assert_eq!(contract["admin"]["route_count"], 19); + assert_eq!(contract["admin"]["model_count"], 32); + assert_eq!(contract["status"]["publisher_count"], 1); + assert_eq!(contract["operations"]["active_probe"], false); + assert_eq!(contract["doctor"]["check_count"], 13); + assert_eq!( + contract["deferred"]["authoritative_daemon_task_graph"], + "unit_15" + ); + for required in [ + "AdminServer::with_system_entropy(router.into_inner(), limits)", + ".pointer(\"/resource_limits/admin\")", + "UnixAdminSocketWriterAuthority::acquire(runtime.context().paths().run())", + "UnixAdminSocketBinding::bind(authority, runtime.artifacts().admin_socket())", + "pub struct MycAdminServer", + "pub struct MycBoundAdminServer", + "pub struct MycAdminCancellationToken", + ] { + assert!( + ADMIN_V1.contains(required), + "Unit 13 is missing `{required}`" + ); + } + for forbidden in [ + "pub fn into_inner", + "pub const fn into_inner", + "pub fn listener", + "TcpListener", + "std::process::exit", + ] { + assert!( + !ADMIN_V1.contains(forbidden), + "Unit 13 exposes forbidden `{forbidden}`" + ); + } +} + +#[test] fn step144_authorization_is_configuration_bound_and_reuses_durable_state() { for forbidden in [ "sqlx::", @@ -1020,10 +1072,11 @@ fn public_errors_remain_crate_owned_redacted_and_source_free() { .lines() .filter(|line| line.starts_with("pub struct myc::") && line.ends_with("Error")) .count(); - assert_eq!(public_error_count, 34); + assert_eq!(public_error_count, 35); assert!(PUBLIC_API.contains("pub struct myc::MycDoctorError")); assert!(PUBLIC_API.contains("pub struct myc::MycConfigApplyError")); assert!(PUBLIC_API.contains("pub struct myc::MycAdminOperationError")); + assert!(PUBLIC_API.contains("pub struct myc::MycAdminServerError")); assert!(!PUBLIC_API.contains("pub struct myc::MycRuntimeFoundation {")); assert!(!PUBLIC_API.contains("pub struct myc::MycStateHost {")); } diff --git a/tests/services_hardening_cli.rs b/tests/services_hardening_cli.rs @@ -80,14 +80,6 @@ fn root_api_freezes_the_exact_command_inventory() { MycCommandV1::Identity(MycIdentityCommandV1::Status), ), ( - vec!["identity", "rekey"], - MycCommandV1::Identity(MycIdentityCommandV1::Rekey), - ), - ( - vec!["identity", "replace"], - MycCommandV1::Identity(MycIdentityCommandV1::Replace), - ), - ( vec!["identity", "export-public"], MycCommandV1::Identity(MycIdentityCommandV1::ExportPublic), ), @@ -235,22 +227,6 @@ fn every_command_has_one_exact_nonforgeable_execution_plan() { true, ), ( - "identity rekey", - vec!["identity", "rekey"], - "live_unix_admin", - None, - Some("/v1/identity/rekey"), - false, - ), - ( - "identity replace", - vec!["identity", "replace"], - "live_unix_admin", - None, - Some("/v1/identity/replace"), - false, - ), - ( "identity export-public", vec!["identity", "export-public"], "live_unix_admin", @@ -403,8 +379,6 @@ fn admin_path(operation: MycCliAdminOperationV1) -> &'static str { MycCliAdminOperationV1::StateStatus => "/v1/state/status", MycCliAdminOperationV1::StateBackup => "/v1/state/backup", MycCliAdminOperationV1::IdentityStatus => "/v1/identity/status", - MycCliAdminOperationV1::IdentityRekey => "/v1/identity/rekey", - MycCliAdminOperationV1::IdentityReplace => "/v1/identity/replace", MycCliAdminOperationV1::IdentityPublic => "/v1/identity/public", } } @@ -429,14 +403,6 @@ fn cli_admin_operations_match_the_governed_route_inventory() { MycAdminRoute::IdentityStatus, ), ( - MycCliAdminOperationV1::IdentityRekey, - MycAdminRoute::IdentityRekey, - ), - ( - MycCliAdminOperationV1::IdentityReplace, - MycAdminRoute::IdentityReplace, - ), - ( MycCliAdminOperationV1::IdentityPublic, MycAdminRoute::IdentityPublic, ), @@ -460,7 +426,7 @@ fn execution_plan_debug_retains_no_bootstrap_or_path_values() { "--config", "/secret/config.toml", "identity", - "rekey", + "export-public", ]) .expect("valid invocation"); let rendered = format!("{invocation:?} {:?}", plan_myc_cli_v1(&invocation)); @@ -480,6 +446,8 @@ fn root_api_rejects_prototype_and_arbitrary_leaf_arguments_safely() { base(&["metrics"]), base(&["persistence", "backup"]), base(&["identity", "generate"]), + base(&["identity", "rekey"]), + base(&["identity", "replace"]), base(&["run", "--relay-url", "wss://secret.example"]), ] { let error = parse_myc_cli_v1_from(arguments).expect_err("forbidden CLI shape"); diff --git a/tests/services_hardening_contracts.rs b/tests/services_hardening_contracts.rs @@ -93,8 +93,6 @@ fn admin_inventory_is_closed_unique_and_model_complete() { "GET|/v1/status|radroots.myc.status.get.v1|empty|service_status_v1|false", "GET|/v1/config/effective|radroots.myc.config.effective.get.v1|empty|effective_config_v1|false", "GET|/v1/identity/status|radroots.myc.identity.status.get.v1|identity_status_query_v1|identity_status_v1|false", - "POST|/v1/identity/rekey|radroots.myc.identity.rekey.v1|identity_rekey_request_v1|identity_mutation_receipt_v1|true", - "POST|/v1/identity/replace|radroots.myc.identity.replace.v1|identity_replace_request_v1|identity_mutation_receipt_v1|true", "GET|/v1/identity/public|radroots.myc.identity.public.get.v1|identity_public_query_v1|identity_public_v1|false", "GET|/v1/state/status|radroots.myc.state.status.get.v1|empty|state_status_v1|false", "POST|/v1/state/backup|radroots.myc.state.backup.create.v1|state_backup_request_v1|state_backup_receipt_v1|true", @@ -278,17 +276,16 @@ fn admin_inventory_is_closed_unique_and_model_complete() { ] ); assert_eq!( - value["admin"]["identity_contract"]["replace_provider_variants"], + value["admin"]["identity_contract"], serde_json::json!({ - "discriminator": "provider", - "encrypted_file": { - "required_fields": ["provider", "envelope_path", "credential_reference", "expected_public_key"], - "provider_value": "encrypted_file" - }, - "local_signer": { - "required_fields": ["provider", "socket_path", "request_deadline_ms", "request_max_bytes", "response_max_bytes", "concurrency", "expected_public_key"], - "provider_value": "local_signer" - } + "roles": [ + { "id": "transport", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, + { "id": "user", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, + { "id": "discovery", "required": false, "disabled_allowed": true, "providers": ["encrypted_file", "local_signer"] } + ], + "live_mutation": false, + "rotation_mode": "offline_create_new_then_config_apply", + "in_place_overwrite": false }) ); assert_eq!( @@ -328,7 +325,7 @@ fn admin_inventory_is_closed_unique_and_model_complete() { assert_eq!(mutation_operations, committed_effects); assert_eq!( decision_sections_digest(&value), - "55890c43b1ad17e897e20afe1df72941589b3d6df554bc3c81731aefe1c55d58" + "4967401cbb7b777aa78e19e91c75fae9d1245a307bf37ea30611d30d8ffeebbd" ); } diff --git a/tests/services_hardening_control_surfaces.rs b/tests/services_hardening_control_surfaces.rs @@ -0,0 +1,82 @@ +#![forbid(unsafe_code)] +#![cfg(any(target_os = "linux", target_os = "macos"))] + +use myc::{ + MYC_DOCTOR_CHECK_COUNT, MYC_LIVEZ_PATH, MYC_METRICS_PATH, MYC_READYZ_PATH, + MycAdminCancellationToken, MycAdminRoute, +}; + +const CONTRACT: &str = include_str!("../contracts/services_hardening/control_surfaces.v1.json"); +const ADMIN_SOURCE: &str = include_str!("../src/admin_v1.rs"); + +#[test] +fn unit_13_contract_binds_the_complete_production_control_surface_inventory() { + let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("control contract"); + assert_eq!(contract["schema"], "radroots.myc.control-surfaces.v1"); + assert_eq!(contract["contract_version"], 1); + assert_eq!(contract["step"], 159); + assert_eq!(contract["unit"], 13); + assert_eq!(contract["unit_id"], "myc-control-surfaces"); + assert_eq!(contract["admin"]["route_count"], 19); + assert_eq!(contract["admin"]["model_count"], 32); + assert_eq!(MycAdminRoute::ALL.len(), 19); + assert_eq!(contract["admin"]["live_identity_mutation_routes"], false); + assert_eq!(contract["status"]["publisher_count"], 1); + assert_eq!(contract["doctor"]["check_count"], MYC_DOCTOR_CHECK_COUNT); + assert_eq!( + contract["operations"]["routes"], + serde_json::json!([MYC_LIVEZ_PATH, MYC_READYZ_PATH, MYC_METRICS_PATH]) + ); + assert_eq!( + contract["deferred"]["authoritative_daemon_task_graph"], + "unit_15" + ); +} + +#[test] +fn admin_server_is_sealed_around_canonical_paths_limits_and_system_entropy() { + let production = ADMIN_SOURCE + .split("\n#[cfg(test)]\nmod tests") + .next() + .expect("production source"); + for required in [ + "AdminServer::with_system_entropy(router.into_inner(), limits)", + ".pointer(\"/resource_limits/admin\")", + "UnixAdminSocketWriterAuthority::acquire(runtime.context().paths().run())", + "UnixAdminSocketBinding::bind(authority, runtime.artifacts().admin_socket())", + "pub struct MycAdminServer", + "pub struct MycBoundAdminServer", + "pub struct MycAdminCancellationToken", + ] { + assert!( + production.contains(required), + "admin source is missing `{required}`" + ); + } + for forbidden in [ + "pub fn into_inner", + "pub const fn into_inner", + "pub fn listener", + "pub fn router", + "TcpListener", + "std::process::exit", + "tokio::spawn", + ] { + assert!( + !production.contains(forbidden), + "admin source exposes forbidden `{forbidden}`" + ); + } +} + +#[test] +fn control_surface_cancellation_is_idempotent_and_redacted() { + let token = MycAdminCancellationToken::new(); + assert!(!token.is_cancelled()); + token.cancel(); + token.cancel(); + assert!(token.is_cancelled()); + let rendered = format!("{token:?}"); + assert!(!rendered.contains("/private/control.sock")); + assert!(!rendered.contains("credential")); +}