commit 9d9d291fdc29cfa0d4fe3278fa1440631c7611cf
parent 4a73308a1cfd57fbe37dc89068565a71721d9a48
Author: triesap <tyson@radroots.org>
Date: Sat, 22 Aug 2026 06:10:59 +0000
status: freeze passive lifecycle cache
Diffstat:
8 files changed, 1821 insertions(+), 1 deletion(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -122,6 +122,14 @@
arbitrary details, unbounded output, detached probe work, or
liveness/readiness probe authority. A pass must prove every contracted scope
facet, and deadline cancellation must stop or synchronously own cleanup.
+- Step 154 freezes one passive latest-value status cache around the shared Lib
+ lifecycle primitive. The non-clone publisher encodes the complete bounded v1
+ local-status envelope before atomic replacement; cloneable readers may only
+ return the retained immutable snapshot. Status reads never query SQLite,
+ providers, relays, credentials, DNS, time, or fresh probes and never spawn a
+ task. Keep connection-count keys and identity roles closed, preserve the last
+ valid snapshot on any failed publication, admit only the fixed twelve status
+ reasons, and keep detailed status on the permissioned Unix-admin boundary.
- 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
@@ -24,6 +24,16 @@ fn forge(_: MycRuntimeFoundation) {}
The reviewed all-features surface is frozen in the
[Myc API baseline](contracts/api_baselines/myc.txt).
+Status publication and cached snapshots can be obtained only through the
+validated factory and its sealed handles:
+
+```compile_fail
+use myc::{MycStatusPublisher, MycStatusSnapshot};
+
+let _publisher = MycStatusPublisher {};
+let _snapshot = MycStatusSnapshot {};
+```
+
The native package and dependency-trust metadata is frozen by
`contracts/services_hardening/native_release.v1.json`. Linux x86_64 and
aarch64 are declared release targets, not qualified artifacts. The canonical
@@ -71,6 +81,19 @@ credentials, public keys, and arbitrary detail strings cannot enter the report.
A pass requires every machine-listed scope facet. Probe futures own their work,
must stop safely when dropped at deadline, and may not detach later mutation.
+The service status boundary uses the shared service-host lifecycle contract
+behind one Myc-owned non-clone publisher and cloneable passive readers. Each
+publication validates the lifecycle transition and encodes the complete
+bounded `service_status_v1` payload before atomically replacing the single
+retained immutable snapshot. Repeated reads return that exact cached value and
+perform no SQLite, provider, relay, credential, DNS, clock, filesystem, or
+fresh-probe work. Connection counts use the closed pending/active/denied/
+expired vocabulary, identity health uses the fixed transport/user/discovery
+roles, failed publication preserves the last valid snapshot, and detailed
+status remains local to the permissioned Unix-admin surface. Status reason
+codes use a closed twelve-value vocabulary; arbitrary strings cannot enter the
+cached response.
+
`parse_myc_config_v1` caps original bytes before decoding, checks the schema
header before closed contract admission, rejects duplicate, null, unknown, and
semantically inconsistent input, and returns an immutable document plus a
diff --git a/contracts/api_baselines/myc.txt b/contracts/api_baselines/myc.txt
@@ -378,6 +378,10 @@ pub myc::MycIdentityCommandV1::Init
pub myc::MycIdentityCommandV1::Rekey
pub myc::MycIdentityCommandV1::Replace
pub myc::MycIdentityCommandV1::Status
+pub enum myc::MycIntegrityStateV1
+pub myc::MycIntegrityStateV1::Failed
+pub myc::MycIntegrityStateV1::VerificationRequired
+pub myc::MycIntegrityStateV1::Verified
pub enum myc::MycLocalSignerTransportErrorKind
pub myc::MycLocalSignerTransportErrorKind::Deadline
pub myc::MycLocalSignerTransportErrorKind::InvalidBinding
@@ -481,6 +485,11 @@ pub enum myc::MycNip46WorkKind
pub myc::MycNip46WorkKind::Connect
pub myc::MycNip46WorkKind::Local
pub myc::MycNip46WorkKind::Provider
+pub enum myc::MycPersistenceHealthV1
+pub myc::MycPersistenceHealthV1::ReadOnly
+pub myc::MycPersistenceHealthV1::Ready
+pub myc::MycPersistenceHealthV1::RepairRequired
+pub myc::MycPersistenceHealthV1::Unavailable
pub enum myc::MycProviderCapability
pub myc::MycProviderCapability::Describe
pub myc::MycProviderCapability::Nip04Decrypt
@@ -501,6 +510,10 @@ pub myc::MycProviderContractErrorKind::InvalidInput
pub myc::MycProviderContractErrorKind::InvalidLimits
pub myc::MycProviderContractErrorKind::InvalidOutput
pub myc::MycProviderContractErrorKind::UnsupportedOperation
+pub enum myc::MycProviderHealthV1
+pub myc::MycProviderHealthV1::Degraded
+pub myc::MycProviderHealthV1::Ready
+pub myc::MycProviderHealthV1::Unavailable
pub enum myc::MycProviderInstanceId
pub myc::MycProviderInstanceId::Discovery
pub myc::MycProviderInstanceId::Transport
@@ -580,6 +593,13 @@ pub myc::MycRuntimeReadinessReason::SignerProviderUnavailable
pub myc::MycRuntimeReadinessReason::SubscriberNotActive
impl myc::MycRuntimeReadinessReason
pub const fn myc::MycRuntimeReadinessReason::as_str(self) -> &'static str
+pub enum myc::MycServicePhase
+pub myc::MycServicePhase::Degraded
+pub myc::MycServicePhase::Failed
+pub myc::MycServicePhase::Ready
+pub myc::MycServicePhase::Starting
+pub myc::MycServicePhase::Stopping
+pub myc::MycServicePhase::Unready
pub enum myc::MycSignerRequestAdmission
pub myc::MycSignerRequestAdmission::Admitted(myc::MycSignerRequestRecord)
pub myc::MycSignerRequestAdmission::ConflictingReuse(myc::MycSignerRequestRecord)
@@ -663,6 +683,50 @@ pub myc::MycStateRepositoryErrorKind::CommitOutcomeUnknown
pub myc::MycStateRepositoryErrorKind::Transaction
impl myc::MycStateRepositoryErrorKind
pub const fn myc::MycStateRepositoryErrorKind::code(self) -> &'static str
+pub enum myc::MycStatusBuildMode
+pub myc::MycStatusBuildMode::Development
+pub myc::MycStatusBuildMode::Release
+pub enum myc::MycStatusConfigurationSource
+pub myc::MycStatusConfigurationSource::DerivedRepoLocal
+pub myc::MycStatusConfigurationSource::ExplicitConfig
+pub enum myc::MycStatusErrorKind
+pub myc::MycStatusErrorKind::Encoding
+pub myc::MycStatusErrorKind::InvalidBuildInfo
+pub myc::MycStatusErrorKind::InvalidConfiguration
+pub myc::MycStatusErrorKind::InvalidIdentityHealth
+pub myc::MycStatusErrorKind::InvalidLifecycle
+pub myc::MycStatusErrorKind::InvalidModel
+pub myc::MycStatusErrorKind::InvalidPersistence
+pub myc::MycStatusErrorKind::InvalidProviderState
+pub myc::MycStatusErrorKind::InvalidReasonCode
+pub myc::MycStatusErrorKind::InvalidTime
+pub myc::MycStatusErrorKind::InvalidTransition
+pub myc::MycStatusErrorKind::InvalidTransportState
+pub myc::MycStatusErrorKind::PublisherDropped
+pub myc::MycStatusErrorKind::ResponseTooLarge
+pub myc::MycStatusErrorKind::TooManyReasonCodes
+impl myc::MycStatusErrorKind
+pub const fn myc::MycStatusErrorKind::code(self) -> &'static str
+pub enum myc::MycStatusReasonCode
+pub myc::MycStatusReasonCode::AdminListenerFailed
+pub myc::MycStatusReasonCode::DatabaseLowDisk
+pub myc::MycStatusReasonCode::DatabaseReadOnly
+pub myc::MycStatusReasonCode::DatabaseSchemaMismatch
+pub myc::MycStatusReasonCode::IdentityUnavailable
+pub myc::MycStatusReasonCode::OperationsListenerFailed
+pub myc::MycStatusReasonCode::OutboxInvariantFailed
+pub myc::MycStatusReasonCode::PublicationBacklogExceeded
+pub myc::MycStatusReasonCode::RequiredRelayUnavailable
+pub myc::MycStatusReasonCode::ShutdownInProgress
+pub myc::MycStatusReasonCode::SignerProviderUnavailable
+pub myc::MycStatusReasonCode::SubscriberNotActive
+impl myc::MycStatusReasonCode
+pub const fn myc::MycStatusReasonCode::as_str(self) -> &'static str
+pub fn myc::MycStatusReasonCode::new(impl core::convert::AsRef<str>) -> core::result::Result<Self, myc::MycStatusError>
+pub enum myc::MycTransportHealthV1
+pub myc::MycTransportHealthV1::Degraded
+pub myc::MycTransportHealthV1::Ready
+pub myc::MycTransportHealthV1::Unavailable
pub struct myc::MycAdminDocumentError
impl myc::MycAdminDocumentError
pub const fn myc::MycAdminDocumentError::kind(self) -> myc::MycAdminDocumentErrorKind
@@ -825,6 +889,13 @@ impl myc::MycConnectionAdmissionRequest
pub fn myc::MycConnectionAdmissionRequest::new(myc::MycSignerOperationId, myc::MycNip46ClientPublicKey, myc::MycConnectionPermissionSet, myc::MycConnectionPolicyGeneration, myc::MycConnectionNonce, myc::MycConnectionTimeUnixMs, core::option::Option<myc::MycConnectionTimeUnixMs>, myc::MycConnectionAdmissionPolicy, myc::MycRateRelayId) -> core::result::Result<Self, myc::MycConnectionStateError>
impl core::fmt::Debug for myc::MycConnectionAdmissionRequest
pub fn myc::MycConnectionAdmissionRequest::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycConnectionCountsV1
+impl myc::MycConnectionCountsV1
+pub const fn myc::MycConnectionCountsV1::active(self) -> u64
+pub const fn myc::MycConnectionCountsV1::denied(self) -> u64
+pub const fn myc::MycConnectionCountsV1::expired(self) -> u64
+pub const fn myc::MycConnectionCountsV1::new(u64, u64, u64, u64) -> Self
+pub const fn myc::MycConnectionCountsV1::pending(self) -> u64
pub struct myc::MycConnectionDecisionRecord
impl myc::MycConnectionDecisionRecord
pub const fn myc::MycConnectionDecisionRecord::connection(&self) -> core::option::Option<&myc::MycConnectionRecord>
@@ -1130,6 +1201,12 @@ impl core::fmt::Debug for myc::MycGovernanceStateError
pub fn myc::MycGovernanceStateError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for myc::MycGovernanceStateError
pub fn myc::MycGovernanceStateError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycIdentityHealthV1
+impl myc::MycIdentityHealthV1
+pub const fn myc::MycIdentityHealthV1::is_available(&self) -> bool
+pub const fn myc::MycIdentityHealthV1::is_configured(&self) -> bool
+pub fn myc::MycIdentityHealthV1::new(bool, bool, myc::MycStatusReasonCodes) -> core::result::Result<Self, myc::MycStatusError>
+pub const fn myc::MycIdentityHealthV1::reason_codes(&self) -> &myc::MycStatusReasonCodes
pub struct myc::MycLocalSignerClient
impl myc::MycLocalSignerClient
pub async fn myc::MycLocalSignerClient::execute(&self, &myc::MycProviderOperation) -> core::result::Result<myc::MycLocalSignerUntrustedResponse, myc::MycLocalSignerTransportError>
@@ -1331,6 +1408,17 @@ impl myc::MycNormalizedConfigDigest
pub const fn myc::MycNormalizedConfigDigest::as_bytes(&self) -> &[u8; 32]
impl core::fmt::Debug for myc::MycNormalizedConfigDigest
pub fn myc::MycNormalizedConfigDigest::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycOutboxStatusV1
+impl myc::MycOutboxStatusV1
+pub const fn myc::MycOutboxStatusV1::new(u64, u64, core::option::Option<myc::MycStatusUnixSeconds>) -> Self
+pub const fn myc::MycOutboxStatusV1::oldest_pending_at_utc(self) -> core::option::Option<myc::MycStatusUnixSeconds>
+pub const fn myc::MycOutboxStatusV1::pending(self) -> u64
+pub const fn myc::MycOutboxStatusV1::unknown(self) -> u64
+pub struct myc::MycPersistenceStatusV1
+impl myc::MycPersistenceStatusV1
+pub fn myc::MycPersistenceStatusV1::new(myc::MycPersistenceHealthV1, u32, u64, myc::MycIntegrityStateV1, myc::MycStatusReasonCodes) -> core::result::Result<Self, myc::MycStatusError>
+impl core::fmt::Debug for myc::MycPersistenceStatusV1
+pub fn myc::MycPersistenceStatusV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct myc::MycPreparedNip46Request
impl myc::MycPreparedNip46Request
pub const fn myc::MycPreparedNip46Request::method(&self) -> myc::MycSignerRequestMethod
@@ -1432,6 +1520,14 @@ pub struct myc::MycProviderResponseObservedAtUnixMs(_)
impl myc::MycProviderResponseObservedAtUnixMs
pub const fn myc::MycProviderResponseObservedAtUnixMs::get(self) -> u64
pub fn myc::MycProviderResponseObservedAtUnixMs::new(u64) -> core::result::Result<Self, myc::MycProviderVerificationError>
+pub struct myc::MycProviderStatusV1
+impl myc::MycProviderStatusV1
+pub const fn myc::MycProviderStatusV1::discovery(&self) -> &myc::MycIdentityHealthV1
+pub const fn myc::MycProviderStatusV1::health(&self) -> myc::MycProviderHealthV1
+pub fn myc::MycProviderStatusV1::new(myc::MycIdentityHealthV1, myc::MycIdentityHealthV1, myc::MycIdentityHealthV1, myc::MycStatusReasonCodes) -> core::result::Result<Self, myc::MycStatusError>
+pub const fn myc::MycProviderStatusV1::reason_codes(&self) -> &myc::MycStatusReasonCodes
+pub const fn myc::MycProviderStatusV1::transport(&self) -> &myc::MycIdentityHealthV1
+pub const fn myc::MycProviderStatusV1::user(&self) -> &myc::MycIdentityHealthV1
pub struct myc::MycProviderVerificationError
impl myc::MycProviderVerificationError
pub const fn myc::MycProviderVerificationError::code(self) -> &'static str
@@ -1452,6 +1548,13 @@ impl myc::MycRateRelayId
pub fn myc::MycRateRelayId::new(&str) -> core::result::Result<Self, myc::MycGovernanceStateError>
impl core::fmt::Debug for myc::MycRateRelayId
pub fn myc::MycRateRelayId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycRelayTransportStatusV1
+impl myc::MycRelayTransportStatusV1
+pub const fn myc::MycRelayTransportStatusV1::connected_relay_count(&self) -> u64
+pub const fn myc::MycRelayTransportStatusV1::health(&self) -> myc::MycTransportHealthV1
+pub fn myc::MycRelayTransportStatusV1::new(myc::MycTransportHealthV1, bool, u64, myc::MycStatusReasonCodes) -> core::result::Result<Self, myc::MycStatusError>
+pub const fn myc::MycRelayTransportStatusV1::reason_codes(&self) -> &myc::MycStatusReasonCodes
+pub const fn myc::MycRelayTransportStatusV1::required_relays_ready(&self) -> bool
pub struct myc::MycReplayBoundNip46Request
impl myc::MycReplayBoundNip46Request
pub fn myc::MycReplayBoundNip46Request::classify_against(&self, myc::MycNip46ReplayKey) -> myc::MycNip46ReplayDisposition
@@ -1664,6 +1767,65 @@ impl core::fmt::Debug for myc::MycStateRepositoryError
pub fn myc::MycStateRepositoryError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for myc::MycStateRepositoryError
pub fn myc::MycStateRepositoryError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusBuildInfoV1
+impl myc::MycStatusBuildInfoV1
+pub fn myc::MycStatusBuildInfoV1::new(myc::MycStatusBuildMode, core::option::Option<&str>, core::option::Option<&str>, core::option::Option<&str>, core::option::Option<&str>, core::option::Option<&str>, core::option::Option<&str>) -> core::result::Result<Self, myc::MycStatusError>
+impl core::fmt::Debug for myc::MycStatusBuildInfoV1
+pub fn myc::MycStatusBuildInfoV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusCommonV1
+impl myc::MycStatusCommonV1
+pub fn myc::MycStatusCommonV1::new(myc::MycServicePhase, bool, myc::MycStatusReasonCodes, u64, myc::MycStatusBuildInfoV1, myc::MycStatusConfigurationIdentityV1, myc::MycPersistenceStatusV1) -> core::result::Result<Self, myc::MycStatusError>
+impl core::fmt::Debug for myc::MycStatusCommonV1
+pub fn myc::MycStatusCommonV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusConfigurationIdentityV1
+impl myc::MycStatusConfigurationIdentityV1
+pub fn myc::MycStatusConfigurationIdentityV1::new(impl core::convert::AsRef<str>, myc::MycStatusConfigurationSource) -> core::result::Result<Self, myc::MycStatusError>
+impl core::fmt::Debug for myc::MycStatusConfigurationIdentityV1
+pub fn myc::MycStatusConfigurationIdentityV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusError
+impl myc::MycStatusError
+pub const fn myc::MycStatusError::code(self) -> &'static str
+pub const fn myc::MycStatusError::kind(self) -> myc::MycStatusErrorKind
+impl core::error::Error for myc::MycStatusError
+impl core::fmt::Debug for myc::MycStatusError
+pub fn myc::MycStatusError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for myc::MycStatusError
+pub fn myc::MycStatusError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusObservationV1
+impl myc::MycStatusObservationV1
+pub fn myc::MycStatusObservationV1::new(myc::MycStatusCommonV1, myc::MycProviderStatusV1, myc::MycRelayTransportStatusV1, myc::MycConnectionCountsV1, myc::MycOutboxStatusV1) -> Self
+impl core::fmt::Debug for myc::MycStatusObservationV1
+pub fn myc::MycStatusObservationV1::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusPublisher
+impl myc::MycStatusPublisher
+pub fn myc::MycStatusPublisher::publish(&mut self, myc::MycStatusObservationV1) -> core::result::Result<(), myc::MycStatusError>
+pub fn myc::MycStatusPublisher::subscribe(&self) -> myc::MycStatusReader
+impl core::fmt::Debug for myc::MycStatusPublisher
+pub fn myc::MycStatusPublisher::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusReader
+impl myc::MycStatusReader
+pub async fn myc::MycStatusReader::changed(&mut self) -> core::result::Result<myc::MycStatusSnapshot, myc::MycStatusError>
+pub fn myc::MycStatusReader::snapshot(&self) -> myc::MycStatusSnapshot
+impl core::clone::Clone for myc::MycStatusReader
+pub fn myc::MycStatusReader::clone(&self) -> Self
+impl core::fmt::Debug for myc::MycStatusReader
+pub fn myc::MycStatusReader::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusReasonCodes(_)
+impl myc::MycStatusReasonCodes
+pub fn myc::MycStatusReasonCodes::as_slice(&self) -> &[myc::MycStatusReasonCode]
+pub const fn myc::MycStatusReasonCodes::empty() -> Self
+pub fn myc::MycStatusReasonCodes::new(impl core::iter::traits::collect::IntoIterator<Item = myc::MycStatusReasonCode>) -> core::result::Result<Self, myc::MycStatusError>
+pub struct myc::MycStatusSnapshot
+impl myc::MycStatusSnapshot
+pub fn myc::MycStatusSnapshot::detailed_status_json(&self) -> &[u8]
+pub fn myc::MycStatusSnapshot::is_ready(&self) -> bool
+pub fn myc::MycStatusSnapshot::phase(&self) -> myc::MycServicePhase
+impl core::fmt::Debug for myc::MycStatusSnapshot
+pub fn myc::MycStatusSnapshot::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct myc::MycStatusUnixSeconds(_)
+impl myc::MycStatusUnixSeconds
+pub const fn myc::MycStatusUnixSeconds::get(self) -> u64
+pub fn myc::MycStatusUnixSeconds::new(u64) -> core::result::Result<Self, myc::MycStatusError>
pub struct myc::MycUntrustedProviderOutput(_)
impl myc::MycUntrustedProviderOutput
pub fn myc::MycUntrustedProviderOutput::as_bytes(&self) -> &[u8]
@@ -1726,6 +1888,7 @@ pub const myc::MYC_DELIVERY_RECOVERY_BATCH_MAX_COUNT: usize
pub const myc::MYC_DELIVERY_RELAY_ID_MAX_BYTES: usize
pub const myc::MYC_DELIVERY_RETRY_JITTER_MAX_MS: u64
pub const myc::MYC_DELIVERY_TARGET_MAX_COUNT: usize
+pub const myc::MYC_DETAILED_STATUS_MAX_UTF8_BYTES: usize
pub const myc::MYC_DISCOVERY_DOCUMENT_MAX_BYTES: usize
pub const myc::MYC_DOCTOR_CHECK_COUNT: usize
pub const myc::MYC_DOCTOR_CONTRACT_VERSION: u32
@@ -1789,6 +1952,8 @@ pub const myc::MYC_STATE_SCHEMA_VERSION_8_SHA256: [u8; 32]
pub const myc::MYC_STATE_SCHEMA_VERSION_9_MIGRATION_SHA256: [u8; 32]
pub const myc::MYC_STATE_SCHEMA_VERSION_9_OBJECT_COUNT: u32
pub const myc::MYC_STATE_SCHEMA_VERSION_9_SHA256: [u8; 32]
+pub const myc::MYC_STATUS_CACHE_CONTRACT_VERSION: u32
+pub const myc::MYC_STATUS_REASON_CODE_COUNT: usize
pub const myc::MYC_WRAPPING_CREDENTIAL_ARTIFACT_BYTES: usize
pub const myc::MYC_WRAPPING_CREDENTIAL_CONTRACT_VERSION: u32
pub trait myc::MycAdminHandler: core::marker::Send + core::marker::Sync + 'static
@@ -1804,6 +1969,7 @@ pub async fn myc::initialize_myc_state(&myc::MycRuntimeContext, &myc::MycStateMe
pub const fn myc::myc_doctor_check_definitions() -> &'static [myc::MycDoctorCheckDefinition; 13]
pub fn myc::myc_migration_catalog() -> core::result::Result<radroots_service_sqlite::migration::MigrationCatalog, myc::MycStateCatalogError>
pub fn myc::myc_schema_catalog() -> core::result::Result<radroots_service_sqlite::integrity::catalog::SchemaCatalog, myc::MycStateCatalogError>
+pub fn myc::myc_status_cache(radroots_runtime_paths::identifier::InstanceId, myc::MycStatusObservationV1) -> core::result::Result<(myc::MycStatusPublisher, myc::MycStatusReader), myc::MycStatusError>
pub fn myc::open_myc_encrypted_identity(&myc::MycProviderBinding, &myc::MycWrappingCredential) -> core::result::Result<myc::MycDecryptedIdentity, myc::MycEncryptedIdentityEnvelopeError>
pub async fn myc::open_myc_runtime_foundation(myc::MycRuntimeContext, myc::MycConfigDocumentV1, myc::MycStateMetadata, radroots_service_sqlite::migration::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<myc::MycRuntimeFoundation, myc::MycRuntimeFoundationError>
pub async fn myc::open_myc_state_inspection(&myc::MycRuntimeContext, &myc::MycStateMetadata) -> core::result::Result<myc::MycStateHost, myc::MycStateHostError>
diff --git a/contracts/services_hardening/status_cache.v1.json b/contracts/services_hardening/status_cache.v1.json
@@ -0,0 +1,46 @@
+{
+ "schema": "radroots.myc.status-cache.v1",
+ "contract_version": 1,
+ "step": 154,
+ "shared_cache": "radroots_service_host::cached_service_state",
+ "publication": {
+ "authority": "single_non_clone_publisher",
+ "capacity": "one_latest_immutable_arc",
+ "replacement": "atomic",
+ "phase_transition": "shared_service_operational_state",
+ "failed_publication": "previous_snapshot_retained"
+ },
+ "read": {
+ "kind": "passive_cached_snapshot",
+ "await_required": false,
+ "sqlite": false,
+ "provider": false,
+ "relay": false,
+ "credential": false,
+ "dns": false,
+ "clock": false,
+ "fresh_probe": false,
+ "task_spawn": false
+ },
+ "detailed_status": {
+ "model": "service_status_v1",
+ "service": "myc",
+ "local_transport": "permissioned_unix_admin_only",
+ "construction": "bounded_encode_before_atomic_publication",
+ "maximum_utf8_bytes": 1048576,
+ "connection_count_keys": ["pending", "active", "denied", "expired"],
+ "identity_roles": ["transport", "user", "discovery"],
+ "reason_codes": ["identity_unavailable", "database_schema_mismatch", "database_read_only", "database_low_disk", "required_relay_unavailable", "subscriber_not_active", "signer_provider_unavailable", "outbox_invariant_failed", "publication_backlog_exceeded", "admin_listener_failed", "operations_listener_failed", "shutdown_in_progress"],
+ "oldest_pending_time": "optional_nonnegative_i64_range_unix_seconds",
+ "protected_material": false,
+ "filesystem_paths": false,
+ "raw_errors": false
+ },
+ "deferred": [
+ "unix_admin_handler_binding",
+ "tcp_operations_binding",
+ "metrics_rendering",
+ "final_runtime_task_graph",
+ "signal_handling"
+ ]
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -35,6 +35,7 @@ mod state_recovery;
mod state_repository;
mod state_request;
mod state_response;
+mod status_v1;
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub use admin_v1::{
@@ -216,3 +217,13 @@ pub use state_response::{
MycNip46ResponseCommitAdmission, MycNip46ResponseCommitError, MycNip46ResponseCommitErrorKind,
MycNip46ResponseCommitRecord, MycNip46ResponseCommitRequest, MycNip46ResponseRecord,
};
+pub use status_v1::{
+ MYC_DETAILED_STATUS_MAX_UTF8_BYTES, MYC_STATUS_CACHE_CONTRACT_VERSION,
+ MYC_STATUS_REASON_CODE_COUNT, MycConnectionCountsV1, MycIdentityHealthV1, MycIntegrityStateV1,
+ MycOutboxStatusV1, MycPersistenceHealthV1, MycPersistenceStatusV1, MycProviderHealthV1,
+ MycProviderStatusV1, MycRelayTransportStatusV1, MycServicePhase, MycStatusBuildInfoV1,
+ MycStatusBuildMode, MycStatusCommonV1, MycStatusConfigurationIdentityV1,
+ MycStatusConfigurationSource, MycStatusError, MycStatusErrorKind, MycStatusObservationV1,
+ MycStatusPublisher, MycStatusReader, MycStatusReasonCode, MycStatusReasonCodes,
+ MycStatusSnapshot, MycStatusUnixSeconds, MycTransportHealthV1, myc_status_cache,
+};
diff --git a/src/status_v1.rs b/src/status_v1.rs
@@ -0,0 +1,1041 @@
+//! Passive, latest-value Myc lifecycle and detailed-status publication.
+
+use core::fmt;
+use std::{error::Error, sync::Arc, time::Duration};
+
+use radroots_service_host::{
+ BuildInfo as HostBuildInfo, BuildInfoEnvironment as HostBuildInfoEnvironment,
+ BuildMode as HostBuildMode, CachedServiceState, CachedServiceStatePublisher,
+ CachedServiceStateReader, ConfigurationIdentity as HostConfigurationIdentity,
+ ConfigurationSource as HostConfigurationSource, ContractVersions as HostContractVersions,
+ InstanceId, IntegrityState as HostIntegrityState, PersistenceHealth as HostPersistenceHealth,
+ PersistenceSummary as HostPersistenceSummary, Readiness as HostReadiness,
+ ReasonCode as HostReasonCode, ReasonCodes as HostReasonCodes, ServiceId,
+ ServiceOperationalState as HostServiceOperationalState, ServicePhase as HostServicePhase,
+ ServiceStatus, ServiceStatusDetail, Sha256Digest as HostSha256Digest, StatusContractError,
+ StatusEncodingError, StatusModelError, UptimeMillis as HostUptimeMillis, cached_service_state,
+};
+use serde::Serialize;
+
+/// Exact version of the passive Myc status-cache contract.
+pub const MYC_STATUS_CACHE_CONTRACT_VERSION: u32 = 1;
+
+/// Maximum encoded byte length of one detailed Myc status response.
+pub const MYC_DETAILED_STATUS_MAX_UTF8_BYTES: usize =
+ radroots_service_host::SERVICE_STATUS_MAX_UTF8_BYTES;
+
+/// Number of stable reason codes admitted by detailed Myc status.
+pub const MYC_STATUS_REASON_CODE_COUNT: usize = 12;
+
+/// One closed stable source-free status reason code.
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum MycStatusReasonCode {
+ IdentityUnavailable,
+ DatabaseSchemaMismatch,
+ DatabaseReadOnly,
+ DatabaseLowDisk,
+ RequiredRelayUnavailable,
+ SubscriberNotActive,
+ SignerProviderUnavailable,
+ OutboxInvariantFailed,
+ PublicationBacklogExceeded,
+ AdminListenerFailed,
+ OperationsListenerFailed,
+ ShutdownInProgress,
+}
+
+impl MycStatusReasonCode {
+ pub fn new(value: impl AsRef<str>) -> Result<Self, MycStatusError> {
+ match value.as_ref() {
+ "identity_unavailable" => Ok(Self::IdentityUnavailable),
+ "database_schema_mismatch" => Ok(Self::DatabaseSchemaMismatch),
+ "database_read_only" => Ok(Self::DatabaseReadOnly),
+ "database_low_disk" => Ok(Self::DatabaseLowDisk),
+ "required_relay_unavailable" => Ok(Self::RequiredRelayUnavailable),
+ "subscriber_not_active" => Ok(Self::SubscriberNotActive),
+ "signer_provider_unavailable" => Ok(Self::SignerProviderUnavailable),
+ "outbox_invariant_failed" => Ok(Self::OutboxInvariantFailed),
+ "publication_backlog_exceeded" => Ok(Self::PublicationBacklogExceeded),
+ "admin_listener_failed" => Ok(Self::AdminListenerFailed),
+ "operations_listener_failed" => Ok(Self::OperationsListenerFailed),
+ "shutdown_in_progress" => Ok(Self::ShutdownInProgress),
+ _ => Err(MycStatusError::new(MycStatusErrorKind::InvalidReasonCode)),
+ }
+ }
+
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::IdentityUnavailable => "identity_unavailable",
+ Self::DatabaseSchemaMismatch => "database_schema_mismatch",
+ Self::DatabaseReadOnly => "database_read_only",
+ Self::DatabaseLowDisk => "database_low_disk",
+ Self::RequiredRelayUnavailable => "required_relay_unavailable",
+ Self::SubscriberNotActive => "subscriber_not_active",
+ Self::SignerProviderUnavailable => "signer_provider_unavailable",
+ Self::OutboxInvariantFailed => "outbox_invariant_failed",
+ Self::PublicationBacklogExceeded => "publication_backlog_exceeded",
+ Self::AdminListenerFailed => "admin_listener_failed",
+ Self::OperationsListenerFailed => "operations_listener_failed",
+ Self::ShutdownInProgress => "shutdown_in_progress",
+ }
+ }
+}
+
+/// Canonically ordered, unique, bounded status reasons.
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
+#[serde(transparent)]
+pub struct MycStatusReasonCodes(Vec<MycStatusReasonCode>);
+
+impl MycStatusReasonCodes {
+ #[must_use]
+ pub const fn empty() -> Self {
+ Self(Vec::new())
+ }
+
+ pub fn new(
+ values: impl IntoIterator<Item = MycStatusReasonCode>,
+ ) -> Result<Self, MycStatusError> {
+ let mut bounded = Vec::with_capacity(MYC_STATUS_REASON_CODE_COUNT);
+ for value in values.into_iter().take(MYC_STATUS_REASON_CODE_COUNT + 1) {
+ if bounded.len() == MYC_STATUS_REASON_CODE_COUNT {
+ return Err(MycStatusError::new(MycStatusErrorKind::TooManyReasonCodes));
+ }
+ bounded.push(value);
+ }
+ bounded.sort_unstable();
+ bounded.dedup();
+ Ok(Self(bounded))
+ }
+
+ #[must_use]
+ pub fn as_slice(&self) -> &[MycStatusReasonCode] {
+ &self.0
+ }
+
+ fn into_host(self) -> Result<HostReasonCodes, MycStatusError> {
+ let values = self
+ .0
+ .into_iter()
+ .map(|value| HostReasonCode::new(value.as_str()).map_err(map_contract_error))
+ .collect::<Result<Vec<_>, _>>()?;
+ HostReasonCodes::new(values).map_err(map_contract_error)
+ }
+}
+
+/// Closed common lifecycle phase used by the Myc public boundary.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum MycServicePhase {
+ Starting,
+ Ready,
+ Degraded,
+ Unready,
+ Stopping,
+ Failed,
+}
+
+impl MycServicePhase {
+ const fn into_host(self) -> HostServicePhase {
+ match self {
+ Self::Starting => HostServicePhase::Starting,
+ Self::Ready => HostServicePhase::Ready,
+ Self::Degraded => HostServicePhase::Degraded,
+ Self::Unready => HostServicePhase::Unready,
+ Self::Stopping => HostServicePhase::Stopping,
+ Self::Failed => HostServicePhase::Failed,
+ }
+ }
+
+ const fn from_host(value: HostServicePhase) -> Self {
+ match value {
+ HostServicePhase::Starting => Self::Starting,
+ HostServicePhase::Ready => Self::Ready,
+ HostServicePhase::Degraded => Self::Degraded,
+ HostServicePhase::Unready => Self::Unready,
+ HostServicePhase::Stopping => Self::Stopping,
+ HostServicePhase::Failed => Self::Failed,
+ }
+ }
+}
+
+/// Build-metadata admission mode for Myc status identity.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycStatusBuildMode {
+ Development,
+ Release,
+}
+
+/// Complete deterministic build identity retained behind the Myc boundary.
+pub struct MycStatusBuildInfoV1 {
+ inner: HostBuildInfo,
+}
+
+impl MycStatusBuildInfoV1 {
+ /// Validates the complete build/source-lock identity with fixed Myc contracts.
+ pub fn new(
+ mode: MycStatusBuildMode,
+ service_version: Option<&str>,
+ service_commit: Option<&str>,
+ lib_revision: Option<&str>,
+ rust_version: Option<&str>,
+ target: Option<&str>,
+ feature_profile: Option<&str>,
+ ) -> Result<Self, MycStatusError> {
+ let contract_versions = HostContractVersions::new(1, 9, 1, 1, 1)
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidBuildInfo))?;
+ HostBuildInfo::from_compile_time(
+ match mode {
+ MycStatusBuildMode::Development => HostBuildMode::Development,
+ MycStatusBuildMode::Release => HostBuildMode::Release,
+ },
+ HostBuildInfoEnvironment {
+ service_version,
+ service_commit,
+ lib_revision,
+ rust_version,
+ target,
+ feature_profile,
+ contract_versions,
+ },
+ )
+ .map(|inner| Self { inner })
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidBuildInfo))
+ }
+}
+
+impl fmt::Debug for MycStatusBuildInfoV1 {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycStatusBuildInfoV1([redacted])")
+ }
+}
+
+/// Exact configuration-source vocabulary exposed by detailed status.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycStatusConfigurationSource {
+ ExplicitConfig,
+ DerivedRepoLocal,
+}
+
+/// Safe configuration identity retained behind the Myc boundary.
+pub struct MycStatusConfigurationIdentityV1 {
+ inner: HostConfigurationIdentity,
+}
+
+impl MycStatusConfigurationIdentityV1 {
+ pub fn new(
+ digest: impl AsRef<str>,
+ source: MycStatusConfigurationSource,
+ ) -> Result<Self, MycStatusError> {
+ let service = ServiceId::new("myc")
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidConfiguration))?;
+ let digest = HostSha256Digest::new(digest)
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidConfiguration))?;
+ HostConfigurationIdentity::for_service(
+ &service,
+ digest,
+ match source {
+ MycStatusConfigurationSource::ExplicitConfig => {
+ HostConfigurationSource::ExplicitConfig
+ }
+ MycStatusConfigurationSource::DerivedRepoLocal => {
+ HostConfigurationSource::DerivedRepoLocal
+ }
+ },
+ )
+ .map(|inner| Self { inner })
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidConfiguration))
+ }
+}
+
+impl fmt::Debug for MycStatusConfigurationIdentityV1 {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycStatusConfigurationIdentityV1([redacted])")
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycPersistenceHealthV1 {
+ Ready,
+ ReadOnly,
+ RepairRequired,
+ Unavailable,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycIntegrityStateV1 {
+ Verified,
+ VerificationRequired,
+ Failed,
+}
+
+/// Validated persistence summary retained behind the Myc boundary.
+pub struct MycPersistenceStatusV1 {
+ inner: HostPersistenceSummary,
+}
+
+impl MycPersistenceStatusV1 {
+ pub fn new(
+ health: MycPersistenceHealthV1,
+ schema_version: u32,
+ generation: u64,
+ integrity: MycIntegrityStateV1,
+ reason_codes: MycStatusReasonCodes,
+ ) -> Result<Self, MycStatusError> {
+ let reason_codes = reason_codes.into_host()?;
+ HostPersistenceSummary::new(
+ match health {
+ MycPersistenceHealthV1::Ready => HostPersistenceHealth::Ready,
+ MycPersistenceHealthV1::ReadOnly => HostPersistenceHealth::ReadOnly,
+ MycPersistenceHealthV1::RepairRequired => HostPersistenceHealth::RepairRequired,
+ MycPersistenceHealthV1::Unavailable => HostPersistenceHealth::Unavailable,
+ },
+ schema_version,
+ generation,
+ match integrity {
+ MycIntegrityStateV1::Verified => HostIntegrityState::Verified,
+ MycIntegrityStateV1::VerificationRequired => {
+ HostIntegrityState::VerificationRequired
+ }
+ MycIntegrityStateV1::Failed => HostIntegrityState::Failed,
+ },
+ reason_codes,
+ )
+ .map(|inner| Self { inner })
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidPersistence))
+ }
+}
+
+impl fmt::Debug for MycPersistenceStatusV1 {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycPersistenceStatusV1([redacted])")
+ }
+}
+
+/// One validated Unix timestamp used only for the oldest pending outbox item.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
+#[serde(transparent)]
+pub struct MycStatusUnixSeconds(u64);
+
+impl MycStatusUnixSeconds {
+ /// Constructs a timestamp representable by SQLite and the frozen wire contract.
+ pub fn new(value: u64) -> Result<Self, MycStatusError> {
+ if value > i64::MAX as u64 {
+ return Err(MycStatusError::new(MycStatusErrorKind::InvalidTime));
+ }
+ Ok(Self(value))
+ }
+
+ /// Returns exact whole Unix seconds.
+ #[must_use]
+ pub const fn get(self) -> u64 {
+ self.0
+ }
+}
+
+/// Passive availability of one configured Myc identity role.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+pub struct MycIdentityHealthV1 {
+ configured: bool,
+ available: bool,
+ reason_codes: MycStatusReasonCodes,
+}
+
+impl MycIdentityHealthV1 {
+ /// Constructs one role observation, rejecting availability without configuration.
+ pub fn new(
+ configured: bool,
+ available: bool,
+ reason_codes: MycStatusReasonCodes,
+ ) -> Result<Self, MycStatusError> {
+ if available && !configured {
+ return Err(MycStatusError::new(
+ MycStatusErrorKind::InvalidIdentityHealth,
+ ));
+ }
+ Ok(Self {
+ configured,
+ available,
+ reason_codes,
+ })
+ }
+
+ #[must_use]
+ pub const fn is_configured(&self) -> bool {
+ self.configured
+ }
+
+ #[must_use]
+ pub const fn is_available(&self) -> bool {
+ self.available
+ }
+
+ #[must_use]
+ pub const fn reason_codes(&self) -> &MycStatusReasonCodes {
+ &self.reason_codes
+ }
+}
+
+/// Complete provider projection for the three fixed Myc identity roles.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+pub struct MycProviderStatusV1 {
+ health: MycProviderHealthV1,
+ transport: MycIdentityHealthV1,
+ user: MycIdentityHealthV1,
+ discovery: MycIdentityHealthV1,
+ reason_codes: MycStatusReasonCodes,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum MycProviderHealthV1 {
+ Ready,
+ Degraded,
+ Unavailable,
+}
+
+impl MycProviderStatusV1 {
+ /// Derives aggregate provider health from the exact role inventory.
+ pub fn new(
+ transport: MycIdentityHealthV1,
+ user: MycIdentityHealthV1,
+ discovery: MycIdentityHealthV1,
+ reason_codes: MycStatusReasonCodes,
+ ) -> Result<Self, MycStatusError> {
+ if !transport.configured || !user.configured {
+ return Err(MycStatusError::new(
+ MycStatusErrorKind::InvalidProviderState,
+ ));
+ }
+ let health = if !transport.available || !user.available {
+ MycProviderHealthV1::Unavailable
+ } else if discovery.configured && !discovery.available {
+ MycProviderHealthV1::Degraded
+ } else {
+ MycProviderHealthV1::Ready
+ };
+ Ok(Self {
+ health,
+ transport,
+ user,
+ discovery,
+ reason_codes,
+ })
+ }
+
+ #[must_use]
+ pub const fn health(&self) -> MycProviderHealthV1 {
+ self.health
+ }
+
+ #[must_use]
+ pub const fn transport(&self) -> &MycIdentityHealthV1 {
+ &self.transport
+ }
+
+ #[must_use]
+ pub const fn user(&self) -> &MycIdentityHealthV1 {
+ &self.user
+ }
+
+ #[must_use]
+ pub const fn discovery(&self) -> &MycIdentityHealthV1 {
+ &self.discovery
+ }
+
+ #[must_use]
+ pub const fn reason_codes(&self) -> &MycStatusReasonCodes {
+ &self.reason_codes
+ }
+}
+
+/// Passive relay-transport projection for detailed status.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+pub struct MycRelayTransportStatusV1 {
+ health: MycTransportHealthV1,
+ required_relays_ready: bool,
+ connected_relay_count: u64,
+ reason_codes: MycStatusReasonCodes,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum MycTransportHealthV1 {
+ Ready,
+ Degraded,
+ Unavailable,
+}
+
+impl MycRelayTransportStatusV1 {
+ /// Constructs one transport observation, rejecting contradictory ready state.
+ pub fn new(
+ health: MycTransportHealthV1,
+ required_relays_ready: bool,
+ connected_relay_count: u64,
+ reason_codes: MycStatusReasonCodes,
+ ) -> Result<Self, MycStatusError> {
+ if health == MycTransportHealthV1::Ready && !required_relays_ready {
+ return Err(MycStatusError::new(
+ MycStatusErrorKind::InvalidTransportState,
+ ));
+ }
+ Ok(Self {
+ health,
+ required_relays_ready,
+ connected_relay_count,
+ reason_codes,
+ })
+ }
+
+ #[must_use]
+ pub const fn health(&self) -> MycTransportHealthV1 {
+ self.health
+ }
+
+ #[must_use]
+ pub const fn required_relays_ready(&self) -> bool {
+ self.required_relays_ready
+ }
+
+ #[must_use]
+ pub const fn connected_relay_count(&self) -> u64 {
+ self.connected_relay_count
+ }
+
+ #[must_use]
+ pub const fn reason_codes(&self) -> &MycStatusReasonCodes {
+ &self.reason_codes
+ }
+}
+
+/// Exact closed connection-count vocabulary rendered as the safe-count map.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
+pub struct MycConnectionCountsV1 {
+ pending: u64,
+ active: u64,
+ denied: u64,
+ expired: u64,
+}
+
+impl MycConnectionCountsV1 {
+ #[must_use]
+ pub const fn new(pending: u64, active: u64, denied: u64, expired: u64) -> Self {
+ Self {
+ pending,
+ active,
+ denied,
+ expired,
+ }
+ }
+
+ #[must_use]
+ pub const fn pending(self) -> u64 {
+ self.pending
+ }
+
+ #[must_use]
+ pub const fn active(self) -> u64 {
+ self.active
+ }
+
+ #[must_use]
+ pub const fn denied(self) -> u64 {
+ self.denied
+ }
+
+ #[must_use]
+ pub const fn expired(self) -> u64 {
+ self.expired
+ }
+}
+
+/// Passive outbox summary derived by an owning runtime task before publication.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
+pub struct MycOutboxStatusV1 {
+ pending: u64,
+ unknown: u64,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ oldest_pending_at_utc: Option<MycStatusUnixSeconds>,
+}
+
+impl MycOutboxStatusV1 {
+ #[must_use]
+ pub const fn new(
+ pending: u64,
+ unknown: u64,
+ oldest_pending_at_utc: Option<MycStatusUnixSeconds>,
+ ) -> Self {
+ Self {
+ pending,
+ unknown,
+ oldest_pending_at_utc,
+ }
+ }
+
+ #[must_use]
+ pub const fn pending(self) -> u64 {
+ self.pending
+ }
+
+ #[must_use]
+ pub const fn unknown(self) -> u64 {
+ self.unknown
+ }
+
+ #[must_use]
+ pub const fn oldest_pending_at_utc(self) -> Option<MycStatusUnixSeconds> {
+ self.oldest_pending_at_utc
+ }
+}
+
+#[derive(Serialize)]
+struct MycStatusDetailV1 {
+ transport: MycIdentityHealthV1,
+ user: MycIdentityHealthV1,
+ discovery: MycIdentityHealthV1,
+ connection_counts: MycConnectionCountsV1,
+ outbox: MycOutboxStatusV1,
+}
+
+impl ServiceStatusDetail for MycStatusDetailV1 {
+ type Provider = MycProviderStatusV1;
+ type Transport = MycRelayTransportStatusV1;
+
+ const FIELD_NAME: &'static str = "myc";
+}
+
+/// Validated common fields shared by one detailed status publication.
+///
+/// Construction and publication perform validation and bounded encoding only.
+/// They do not query SQLite, providers, relays, DNS, credentials, or the clock.
+pub struct MycStatusCommonV1 {
+ operational: HostServiceOperationalState,
+ uptime: HostUptimeMillis,
+ build: HostBuildInfo,
+ configuration: HostConfigurationIdentity,
+ persistence: HostPersistenceSummary,
+}
+
+impl MycStatusCommonV1 {
+ /// Validates the common lifecycle and detailed-status envelope fields.
+ pub fn new(
+ phase: MycServicePhase,
+ ready: bool,
+ reason_codes: MycStatusReasonCodes,
+ uptime_millis: u64,
+ build: MycStatusBuildInfoV1,
+ configuration: MycStatusConfigurationIdentityV1,
+ persistence: MycPersistenceStatusV1,
+ ) -> Result<Self, MycStatusError> {
+ let operational = HostServiceOperationalState::new(
+ phase.into_host(),
+ if ready {
+ HostReadiness::READY
+ } else {
+ HostReadiness::NOT_READY
+ },
+ reason_codes.into_host()?,
+ )
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidLifecycle))?;
+ let uptime = HostUptimeMillis::from_duration(Duration::from_millis(uptime_millis))
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidTime))?;
+ Ok(Self {
+ operational,
+ uptime,
+ build: build.inner,
+ configuration: configuration.inner,
+ persistence: persistence.inner,
+ })
+ }
+}
+
+impl fmt::Debug for MycStatusCommonV1 {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycStatusCommonV1([redacted])")
+ }
+}
+
+/// One complete, already-observed status publication input.
+pub struct MycStatusObservationV1 {
+ common: MycStatusCommonV1,
+ provider: MycProviderStatusV1,
+ transport: MycRelayTransportStatusV1,
+ connection_counts: MycConnectionCountsV1,
+ outbox: MycOutboxStatusV1,
+}
+
+impl MycStatusObservationV1 {
+ #[must_use]
+ pub fn new(
+ common: MycStatusCommonV1,
+ provider: MycProviderStatusV1,
+ transport: MycRelayTransportStatusV1,
+ connection_counts: MycConnectionCountsV1,
+ outbox: MycOutboxStatusV1,
+ ) -> Self {
+ Self {
+ common,
+ provider,
+ transport,
+ connection_counts,
+ outbox,
+ }
+ }
+
+ fn into_cached(
+ self,
+ instance: &InstanceId,
+ ) -> Result<CachedServiceState<MycCachedStatus>, MycStatusError> {
+ let operational = self.common.operational.clone();
+ let detail = MycStatusDetailV1 {
+ transport: self.provider.transport.clone(),
+ user: self.provider.user.clone(),
+ discovery: self.provider.discovery.clone(),
+ connection_counts: self.connection_counts,
+ outbox: self.outbox,
+ };
+ let service = ServiceId::new("myc")
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidModel))?;
+ let status = ServiceStatus::new(
+ service,
+ instance.clone(),
+ self.common.operational,
+ self.common.uptime,
+ self.common.build,
+ self.common.configuration,
+ self.common.persistence,
+ self.provider,
+ self.transport,
+ detail,
+ )
+ .map_err(map_model_error)?;
+ let json = status.to_bounded_json().map_err(map_encoding_error)?;
+ Ok(CachedServiceState::new(
+ operational,
+ MycCachedStatus {
+ json: json.into_boxed_slice(),
+ },
+ ))
+ }
+}
+
+impl fmt::Debug for MycStatusObservationV1 {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycStatusObservationV1([redacted])")
+ }
+}
+
+struct MycCachedStatus {
+ json: Box<[u8]>,
+}
+
+impl fmt::Debug for MycCachedStatus {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MycCachedStatus")
+ .field("json_utf8_bytes", &self.json.len())
+ .finish()
+ }
+}
+
+/// Sole publication authority for one process-local Myc status cache.
+///
+/// This type deliberately does not implement `Clone`. A successful publish
+/// atomically replaces the one retained snapshot. A failed encoding or illegal
+/// lifecycle transition leaves the previous snapshot unchanged.
+pub struct MycStatusPublisher {
+ instance: InstanceId,
+ inner: CachedServiceStatePublisher<MycCachedStatus>,
+}
+
+impl MycStatusPublisher {
+ /// Encodes and atomically publishes one already-observed status value.
+ pub fn publish(&mut self, next: MycStatusObservationV1) -> Result<(), MycStatusError> {
+ let next = next.into_cached(&self.instance)?;
+ self.inner.publish(next).map_err(map_contract_error)
+ }
+
+ /// Creates another passive reader without sharing publication authority.
+ #[must_use]
+ pub fn subscribe(&self) -> MycStatusReader {
+ MycStatusReader {
+ inner: self.inner.subscribe(),
+ }
+ }
+}
+
+impl fmt::Debug for MycStatusPublisher {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycStatusPublisher([sealed])")
+ }
+}
+
+/// Cloneable passive reader of the latest Myc lifecycle and detailed status.
+pub struct MycStatusReader {
+ inner: CachedServiceStateReader<MycCachedStatus>,
+}
+
+impl Clone for MycStatusReader {
+ fn clone(&self) -> Self {
+ Self {
+ inner: self.inner.clone(),
+ }
+ }
+}
+
+impl MycStatusReader {
+ /// Returns the latest immutable snapshot without awaiting or probing.
+ #[must_use]
+ pub fn snapshot(&self) -> MycStatusSnapshot {
+ MycStatusSnapshot {
+ inner: self.inner.snapshot(),
+ }
+ }
+
+ /// Waits for a later publication and returns the newest retained value.
+ pub async fn changed(&mut self) -> Result<MycStatusSnapshot, MycStatusError> {
+ self.inner
+ .changed()
+ .await
+ .map(|inner| MycStatusSnapshot { inner })
+ .map_err(|_| MycStatusError::new(MycStatusErrorKind::PublisherDropped))
+ }
+}
+
+impl fmt::Debug for MycStatusReader {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("MycStatusReader([passive])")
+ }
+}
+
+/// One immutable point-in-time status snapshot backed by the retained cache `Arc`.
+pub struct MycStatusSnapshot {
+ inner: Arc<CachedServiceState<MycCachedStatus>>,
+}
+
+impl MycStatusSnapshot {
+ #[must_use]
+ pub fn phase(&self) -> MycServicePhase {
+ MycServicePhase::from_host(self.inner.operational().phase())
+ }
+
+ #[must_use]
+ pub fn is_ready(&self) -> bool {
+ self.inner.operational().readiness().is_ready()
+ }
+
+ /// Returns the already-bounded canonical detailed-status JSON bytes.
+ #[must_use]
+ pub fn detailed_status_json(&self) -> &[u8] {
+ &self.inner.metrics().json
+ }
+}
+
+impl fmt::Debug for MycStatusSnapshot {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MycStatusSnapshot")
+ .field("phase", &self.phase())
+ .field("ready", &self.is_ready())
+ .field("json_utf8_bytes", &self.detailed_status_json().len())
+ .finish()
+ }
+}
+
+/// Creates the single-writer, one-latest-value Myc status cache.
+pub fn myc_status_cache(
+ instance: InstanceId,
+ initial: MycStatusObservationV1,
+) -> Result<(MycStatusPublisher, MycStatusReader), MycStatusError> {
+ let initial = initial.into_cached(&instance)?;
+ let (inner, reader) = cached_service_state(initial);
+ Ok((
+ MycStatusPublisher { instance, inner },
+ MycStatusReader { inner: reader },
+ ))
+}
+
+/// Stable source-free status failure category.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycStatusErrorKind {
+ InvalidReasonCode,
+ TooManyReasonCodes,
+ InvalidLifecycle,
+ InvalidBuildInfo,
+ InvalidConfiguration,
+ InvalidPersistence,
+ InvalidIdentityHealth,
+ InvalidProviderState,
+ InvalidTransportState,
+ InvalidTime,
+ InvalidModel,
+ Encoding,
+ ResponseTooLarge,
+ InvalidTransition,
+ PublisherDropped,
+}
+
+impl MycStatusErrorKind {
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidReasonCode => "status_reason_code_invalid",
+ Self::TooManyReasonCodes => "status_reason_count_exceeded",
+ Self::InvalidLifecycle => "status_lifecycle_invalid",
+ Self::InvalidBuildInfo => "status_build_info_invalid",
+ Self::InvalidConfiguration => "status_configuration_invalid",
+ Self::InvalidPersistence => "status_persistence_invalid",
+ Self::InvalidIdentityHealth => "status_identity_health_invalid",
+ Self::InvalidProviderState => "status_provider_state_invalid",
+ Self::InvalidTransportState => "status_transport_state_invalid",
+ Self::InvalidTime => "status_time_invalid",
+ Self::InvalidModel => "status_model_invalid",
+ Self::Encoding => "status_encoding_failed",
+ Self::ResponseTooLarge => "status_response_too_large",
+ Self::InvalidTransition => "status_transition_invalid",
+ Self::PublisherDropped => "status_publisher_dropped",
+ }
+ }
+
+ const fn message(self) -> &'static str {
+ match self {
+ Self::InvalidReasonCode => "Myc status reason code is invalid",
+ Self::TooManyReasonCodes => "Myc status has too many reason codes",
+ Self::InvalidLifecycle => "Myc lifecycle status is invalid",
+ Self::InvalidBuildInfo => "Myc status build identity is invalid",
+ Self::InvalidConfiguration => "Myc status configuration identity is invalid",
+ Self::InvalidPersistence => "Myc persistence status is invalid",
+ Self::InvalidIdentityHealth => "Myc identity health is invalid",
+ Self::InvalidProviderState => "Myc provider status is invalid",
+ Self::InvalidTransportState => "Myc transport status is invalid",
+ Self::InvalidTime => "Myc status time is invalid",
+ Self::InvalidModel => "Myc detailed status is invalid",
+ Self::Encoding => "Myc detailed status encoding failed",
+ Self::ResponseTooLarge => "Myc detailed status exceeds its byte limit",
+ Self::InvalidTransition => "Myc lifecycle transition is invalid",
+ Self::PublisherDropped => "Myc status publisher is unavailable",
+ }
+ }
+}
+
+/// One redacted source-free status failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct MycStatusError {
+ kind: MycStatusErrorKind,
+}
+
+impl MycStatusError {
+ const fn new(kind: MycStatusErrorKind) -> Self {
+ Self { kind }
+ }
+
+ #[must_use]
+ pub const fn kind(self) -> MycStatusErrorKind {
+ self.kind
+ }
+
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.kind.code()
+ }
+}
+
+impl fmt::Debug for MycStatusError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MycStatusError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl fmt::Display for MycStatusError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(self.kind.message())
+ }
+}
+
+impl Error for MycStatusError {}
+
+const fn map_model_error(_error: StatusModelError) -> MycStatusError {
+ MycStatusError::new(MycStatusErrorKind::InvalidModel)
+}
+
+const fn map_encoding_error(error: StatusEncodingError) -> MycStatusError {
+ match error {
+ StatusEncodingError::EncodingFailed => MycStatusError::new(MycStatusErrorKind::Encoding),
+ StatusEncodingError::ResponseTooLarge => {
+ MycStatusError::new(MycStatusErrorKind::ResponseTooLarge)
+ }
+ }
+}
+
+const fn map_contract_error(_error: StatusContractError) -> MycStatusError {
+ MycStatusError::new(MycStatusErrorKind::InvalidTransition)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn invalid_status_inputs_fail_with_safe_source_free_errors() {
+ assert_eq!(
+ MycIdentityHealthV1::new(false, true, MycStatusReasonCodes::empty()),
+ Err(MycStatusError::new(
+ MycStatusErrorKind::InvalidIdentityHealth
+ ))
+ );
+ let unavailable =
+ MycIdentityHealthV1::new(true, false, MycStatusReasonCodes::empty()).unwrap();
+ assert_eq!(
+ MycProviderStatusV1::new(
+ MycIdentityHealthV1::new(false, false, MycStatusReasonCodes::empty()).unwrap(),
+ unavailable,
+ MycIdentityHealthV1::new(false, false, MycStatusReasonCodes::empty()).unwrap(),
+ MycStatusReasonCodes::empty(),
+ ),
+ Err(MycStatusError::new(
+ MycStatusErrorKind::InvalidProviderState
+ ))
+ );
+ assert_eq!(
+ MycRelayTransportStatusV1::new(
+ MycTransportHealthV1::Ready,
+ false,
+ 0,
+ MycStatusReasonCodes::empty(),
+ ),
+ Err(MycStatusError::new(
+ MycStatusErrorKind::InvalidTransportState
+ ))
+ );
+ assert_eq!(
+ MycStatusUnixSeconds::new(i64::MAX as u64 + 1),
+ Err(MycStatusError::new(MycStatusErrorKind::InvalidTime))
+ );
+ for kind in [
+ MycStatusErrorKind::InvalidReasonCode,
+ MycStatusErrorKind::TooManyReasonCodes,
+ MycStatusErrorKind::InvalidLifecycle,
+ MycStatusErrorKind::InvalidBuildInfo,
+ MycStatusErrorKind::InvalidConfiguration,
+ MycStatusErrorKind::InvalidPersistence,
+ MycStatusErrorKind::InvalidIdentityHealth,
+ MycStatusErrorKind::InvalidProviderState,
+ MycStatusErrorKind::InvalidTransportState,
+ MycStatusErrorKind::InvalidTime,
+ MycStatusErrorKind::InvalidModel,
+ MycStatusErrorKind::Encoding,
+ MycStatusErrorKind::ResponseTooLarge,
+ MycStatusErrorKind::InvalidTransition,
+ MycStatusErrorKind::PublisherDropped,
+ ] {
+ let error = MycStatusError::new(kind);
+ assert!(!error.code().is_empty());
+ assert!(Error::source(&error).is_none());
+ assert!(!format!("{error} {error:?}").contains("source"));
+ }
+ }
+}
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -15,6 +15,8 @@ const NIP46_COMPLETION: &str = include_str!("../src/state_completion.rs");
const NIP46_RESPONSE: &str = include_str!("../src/state_response.rs");
const DELIVERY_RECOVERY: &str = include_str!("../src/state_recovery.rs");
const DOCTOR_V1: &str = include_str!("../src/doctor_v1.rs");
+const STATUS_V1: &str = include_str!("../src/status_v1.rs");
+const STATUS_CONTRACT: &str = include_str!("../contracts/services_hardening/status_cache.v1.json");
const DISCOVERY_STATE: &str = include_str!("../src/state_discovery.rs");
const NIP46_VERIFICATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/nip46_verification.v1.json");
@@ -49,6 +51,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/provider_verification.rs"),
include_str!("../src/runtime_context.rs"),
include_str!("../src/runtime_foundation.rs"),
+ include_str!("../src/status_v1.rs"),
include_str!("../src/state_catalog.rs"),
include_str!("../src/state_completion.rs"),
include_str!("../src/state_connection.rs"),
@@ -90,6 +93,7 @@ fn implementation_modules_are_private_and_rustdoc_uses_the_reviewed_readme() {
"provider_verification",
"runtime_context",
"runtime_foundation",
+ "status_v1",
"state_catalog",
"state_completion",
"state_connection",
@@ -114,6 +118,7 @@ fn implementation_modules_are_private_and_rustdoc_uses_the_reviewed_readme() {
"public errors use Myc-owned stable classifications",
"```compile_fail",
"[Myc API baseline](contracts/api_baselines/myc.txt)",
+ "Status publication and cached snapshots can be obtained only",
] {
assert!(README.contains(required), "README is missing `{required}`");
}
@@ -137,6 +142,12 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() {
"pub enum myc::MycDoctorRemediationCode",
"pub trait myc::MycDoctorProbe",
"pub async fn myc::run_myc_doctor",
+ "pub struct myc::MycStatusPublisher",
+ "pub struct myc::MycStatusReader",
+ "pub struct myc::MycStatusSnapshot",
+ "pub struct myc::MycStatusCommonV1",
+ "pub struct myc::MycStatusObservationV1",
+ "pub fn myc::myc_status_cache",
"pub struct myc::MycAdminRequestDocument",
"pub struct myc::MycAdminResponseDocument",
"pub enum myc::MycAdminMethod",
@@ -224,6 +235,7 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() {
"provider_verification",
"runtime_context",
"runtime_foundation",
+ "status_v1",
"state_catalog",
"state_completion",
"state_connection",
@@ -271,6 +283,62 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() {
}
#[test]
+fn status_cache_is_passive_latest_value_and_dependency_neutral() {
+ for required in [
+ "CachedServiceStatePublisher<MycCachedStatus>",
+ "pub struct MycStatusPublisher",
+ "pub struct MycStatusReader",
+ "pub struct MycStatusSnapshot",
+ "pub fn myc_status_cache(",
+ "status.to_bounded_json()",
+ "self.inner.publish(next)",
+ "self.inner.snapshot()",
+ "connection_counts: MycConnectionCountsV1",
+ "oldest_pending_at_utc: Option<MycStatusUnixSeconds>",
+ "MYC_STATUS_REASON_CODE_COUNT: usize = 12",
+ ] {
+ assert!(STATUS_V1.contains(required), "missing `{required}`");
+ }
+ for forbidden in [
+ "sqlx::",
+ "std::fs::",
+ "tokio::spawn",
+ "spawn_blocking",
+ "std::time::SystemTime",
+ "std::env::",
+ "reqwest::",
+ "url::Url",
+ "provider.execute",
+ "relay.connect",
+ "dns",
+ ] {
+ assert!(!STATUS_V1.contains(forbidden), "found `{forbidden}`");
+ }
+ assert!(PUBLIC_API.contains("impl core::clone::Clone for myc::MycStatusReader"));
+ assert!(!PUBLIC_API.contains("impl core::clone::Clone for myc::MycStatusPublisher"));
+ assert!(!PUBLIC_API.contains("impl core::clone::Clone for myc::MycStatusSnapshot"));
+ for required in [
+ "identity_unavailable",
+ "database_schema_mismatch",
+ "database_read_only",
+ "database_low_disk",
+ "required_relay_unavailable",
+ "subscriber_not_active",
+ "signer_provider_unavailable",
+ "outbox_invariant_failed",
+ "publication_backlog_exceeded",
+ "admin_listener_failed",
+ "operations_listener_failed",
+ "shutdown_in_progress",
+ ] {
+ assert!(
+ STATUS_CONTRACT.contains(required),
+ "status contract is missing `{required}`"
+ );
+ }
+}
+
+#[test]
fn doctor_boundary_is_closed_bounded_and_dependency_neutral() {
for required in [
"MYC_DOCTOR_CHECK_COUNT: usize = 13",
@@ -642,7 +710,7 @@ 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, 28);
+ assert_eq!(public_error_count, 29);
assert!(PUBLIC_API.contains("pub struct myc::MycDoctorError"));
assert!(!PUBLIC_API.contains("pub struct myc::MycRuntimeFoundation {"));
assert!(!PUBLIC_API.contains("pub struct myc::MycStateHost {"));
diff --git a/tests/services_hardening_status.rs b/tests/services_hardening_status.rs
@@ -0,0 +1,457 @@
+#![forbid(unsafe_code)]
+
+use std::error::Error;
+
+use myc::{
+ InstanceId, MYC_DETAILED_STATUS_MAX_UTF8_BYTES, MYC_STATUS_CACHE_CONTRACT_VERSION,
+ MycConnectionCountsV1, MycIdentityHealthV1, MycIntegrityStateV1, MycOutboxStatusV1,
+ MycPersistenceHealthV1, MycPersistenceStatusV1, MycProviderStatusV1, MycRelayTransportStatusV1,
+ MycServicePhase, MycStatusBuildInfoV1, MycStatusBuildMode, MycStatusCommonV1,
+ MycStatusConfigurationIdentityV1, MycStatusConfigurationSource, MycStatusErrorKind,
+ MycStatusObservationV1, MycStatusReasonCode, MycStatusReasonCodes, MycStatusUnixSeconds,
+ MycTransportHealthV1, myc_status_cache,
+};
+
+const CONTRACT: &str = include_str!("../contracts/services_hardening/status_cache.v1.json");
+const SERVICE_REVISION: &str = "0123456789abcdef0123456789abcdef01234567";
+const LIB_REVISION: &str = "89abcdef0123456789abcdef0123456789abcdef";
+
+fn reasons(values: &[&str]) -> MycStatusReasonCodes {
+ MycStatusReasonCodes::new(
+ values
+ .iter()
+ .map(|value| MycStatusReasonCode::new(value).expect("reason code")),
+ )
+ .expect("reason codes")
+}
+
+fn build_info() -> MycStatusBuildInfoV1 {
+ MycStatusBuildInfoV1::new(
+ MycStatusBuildMode::Release,
+ Some("0.1.0"),
+ Some(SERVICE_REVISION),
+ Some(LIB_REVISION),
+ Some("1.97.1"),
+ Some("x86_64-unknown-linux-gnu"),
+ Some("service-host"),
+ )
+ .expect("build info")
+}
+
+fn identity(configured: bool, available: bool, reason: &[&str]) -> MycIdentityHealthV1 {
+ MycIdentityHealthV1::new(configured, available, reasons(reason)).expect("identity health")
+}
+
+fn observation(
+ phase: MycServicePhase,
+ ready: bool,
+ uptime_ms: u64,
+ pending_connections: u64,
+) -> MycStatusObservationV1 {
+ let lifecycle_reasons = if phase == MycServicePhase::Degraded {
+ reasons(&["required_relay_unavailable"])
+ } else {
+ MycStatusReasonCodes::empty()
+ };
+ let configuration = MycStatusConfigurationIdentityV1::new(
+ "a".repeat(64),
+ MycStatusConfigurationSource::ExplicitConfig,
+ )
+ .expect("configuration");
+ let persistence = MycPersistenceStatusV1::new(
+ MycPersistenceHealthV1::Ready,
+ 9,
+ 42,
+ MycIntegrityStateV1::Verified,
+ MycStatusReasonCodes::empty(),
+ )
+ .expect("persistence");
+ let provider = MycProviderStatusV1::new(
+ identity(true, true, &[]),
+ identity(true, true, &[]),
+ identity(false, false, &[]),
+ MycStatusReasonCodes::empty(),
+ )
+ .expect("provider");
+ let transport = MycRelayTransportStatusV1::new(
+ if phase == MycServicePhase::Degraded {
+ MycTransportHealthV1::Degraded
+ } else {
+ MycTransportHealthV1::Ready
+ },
+ phase != MycServicePhase::Degraded,
+ 2,
+ if phase == MycServicePhase::Degraded {
+ reasons(&["required_relay_unavailable"])
+ } else {
+ MycStatusReasonCodes::empty()
+ },
+ )
+ .expect("transport");
+ MycStatusObservationV1::new(
+ MycStatusCommonV1::new(
+ phase,
+ ready,
+ lifecycle_reasons,
+ uptime_ms,
+ build_info(),
+ configuration,
+ persistence,
+ )
+ .expect("common status"),
+ provider,
+ transport,
+ MycConnectionCountsV1::new(pending_connections, 3, 1, 2),
+ MycOutboxStatusV1::new(
+ 4,
+ 1,
+ Some(MycStatusUnixSeconds::new(1_723_456_789).expect("outbox time")),
+ ),
+ )
+}
+
+#[test]
+fn machine_contract_and_canonical_detailed_status_are_exact() {
+ let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("status contract");
+ assert_eq!(contract["schema"], "radroots.myc.status-cache.v1");
+ assert_eq!(
+ contract["contract_version"],
+ MYC_STATUS_CACHE_CONTRACT_VERSION
+ );
+ assert_eq!(contract["step"], 154);
+ assert_eq!(
+ contract["publication"]["capacity"],
+ "one_latest_immutable_arc"
+ );
+ assert_eq!(contract["read"]["fresh_probe"], false);
+ assert_eq!(
+ contract["detailed_status"]["maximum_utf8_bytes"],
+ MYC_DETAILED_STATUS_MAX_UTF8_BYTES
+ );
+ assert_eq!(
+ contract["detailed_status"]["reason_codes"]
+ .as_array()
+ .expect("reason inventory")
+ .iter()
+ .map(|value| value.as_str().expect("reason"))
+ .collect::<Vec<_>>(),
+ [
+ MycStatusReasonCode::IdentityUnavailable,
+ MycStatusReasonCode::DatabaseSchemaMismatch,
+ MycStatusReasonCode::DatabaseReadOnly,
+ MycStatusReasonCode::DatabaseLowDisk,
+ MycStatusReasonCode::RequiredRelayUnavailable,
+ MycStatusReasonCode::SubscriberNotActive,
+ MycStatusReasonCode::SignerProviderUnavailable,
+ MycStatusReasonCode::OutboxInvariantFailed,
+ MycStatusReasonCode::PublicationBacklogExceeded,
+ MycStatusReasonCode::AdminListenerFailed,
+ MycStatusReasonCode::OperationsListenerFailed,
+ MycStatusReasonCode::ShutdownInProgress,
+ ]
+ .map(MycStatusReasonCode::as_str)
+ );
+
+ let (_publisher, reader) = myc_status_cache(
+ InstanceId::new("primary").expect("instance"),
+ observation(MycServicePhase::Ready, true, 120_000, 5),
+ )
+ .expect("status cache");
+ let snapshot = reader.snapshot();
+ let wire = std::str::from_utf8(snapshot.detailed_status_json()).expect("status UTF-8");
+ assert_eq!(
+ wire,
+ r#"{"contract_version":1,"service":"myc","instance":"primary","phase":"ready","ready":true,"uptime_millis":120000,"reason_codes":[],"build_info":{"version":"0.1.0","revision":"0123456789abcdef0123456789abcdef01234567","toolchain":"1.97.1","contract_versions":{"config":1,"state":9,"admin":1,"status":1,"provider":1}},"configuration":{"schema":"radroots.myc.config","schema_version":1,"digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","source":"explicit_config"},"persistence":{"health":"ready","schema_version":9,"generation":42,"integrity":"verified","reason_codes":[]},"provider":{"health":"ready","transport":{"configured":true,"available":true,"reason_codes":[]},"user":{"configured":true,"available":true,"reason_codes":[]},"discovery":{"configured":false,"available":false,"reason_codes":[]},"reason_codes":[]},"transport":{"health":"ready","required_relays_ready":true,"connected_relay_count":2,"reason_codes":[]},"myc":{"transport":{"configured":true,"available":true,"reason_codes":[]},"user":{"configured":true,"available":true,"reason_codes":[]},"discovery":{"configured":false,"available":false,"reason_codes":[]},"connection_counts":{"pending":5,"active":3,"denied":1,"expired":2},"outbox":{"pending":4,"unknown":1,"oldest_pending_at_utc":1723456789}}}"#
+ );
+ assert!(wire.len() < MYC_DETAILED_STATUS_MAX_UTF8_BYTES);
+ for forbidden in [
+ "secret",
+ "credential",
+ "private_key",
+ "password",
+ "filesystem_path",
+ "relay_url",
+ "raw_error",
+ ] {
+ assert!(!wire.contains(forbidden), "wire leaked `{forbidden}`");
+ }
+}
+
+#[tokio::test]
+async fn latest_publication_is_atomic_passive_and_retains_old_snapshots() {
+ let (mut publisher, reader) = myc_status_cache(
+ InstanceId::new("primary").expect("instance"),
+ observation(MycServicePhase::Starting, false, 0, 9),
+ )
+ .expect("status cache");
+ let old = reader.snapshot();
+ let old_pointer = old.detailed_status_json().as_ptr();
+ for _ in 0..1_000 {
+ let same = reader.snapshot();
+ assert_eq!(same.detailed_status_json().as_ptr(), old_pointer);
+ assert_eq!(same.phase(), MycServicePhase::Starting);
+ }
+
+ let mut changed = publisher.subscribe();
+ publisher
+ .publish(observation(MycServicePhase::Ready, true, 10, 2))
+ .expect("ready publication");
+ publisher
+ .publish(observation(MycServicePhase::Degraded, true, 20, 1))
+ .expect("degraded publication");
+
+ let latest = changed.changed().await.expect("latest publication");
+ assert_eq!(latest.phase(), MycServicePhase::Degraded);
+ assert!(latest.is_ready());
+ assert!(
+ std::str::from_utf8(latest.detailed_status_json())
+ .expect("status UTF-8")
+ .contains("\"uptime_millis\":20")
+ );
+ assert_eq!(old.phase(), MycServicePhase::Starting);
+ assert!(
+ std::str::from_utf8(old.detailed_status_json())
+ .expect("old UTF-8")
+ .contains("\"uptime_millis\":0")
+ );
+}
+
+#[tokio::test]
+async fn illegal_transition_and_publisher_drop_preserve_the_last_valid_value() {
+ let (mut publisher, reader) = myc_status_cache(
+ InstanceId::new("primary").expect("instance"),
+ observation(MycServicePhase::Starting, false, 1, 0),
+ )
+ .expect("status cache");
+ let before = reader.snapshot().detailed_status_json().to_vec();
+ let error = publisher
+ .publish(observation(MycServicePhase::Unready, false, 2, 0))
+ .expect_err("illegal starting to unready transition");
+ assert_eq!(error.kind(), MycStatusErrorKind::InvalidTransition);
+ assert_eq!(reader.snapshot().detailed_status_json(), before);
+ assert!(Error::source(&error).is_none());
+
+ let mut dropped = publisher.subscribe();
+ drop(publisher);
+ let dropped_error = dropped.changed().await.expect_err("publisher dropped");
+ assert_eq!(dropped_error.kind(), MycStatusErrorKind::PublisherDropped);
+ assert_eq!(reader.snapshot().detailed_status_json(), before);
+}
+
+#[test]
+fn closed_role_counts_time_and_safe_debug_bound_the_status_surface() {
+ let counts = MycConnectionCountsV1::new(u64::MAX, 2, 3, 4);
+ assert_eq!(counts.pending(), u64::MAX);
+ assert_eq!(counts.active(), 2);
+ assert_eq!(counts.denied(), 3);
+ assert_eq!(counts.expired(), 4);
+ assert_eq!(MycStatusUnixSeconds::new(0).expect("zero").get(), 0);
+ assert_eq!(
+ MycStatusUnixSeconds::new(i64::MAX as u64)
+ .expect("maximum")
+ .get(),
+ i64::MAX as u64
+ );
+ assert_eq!(
+ MycStatusUnixSeconds::new(i64::MAX as u64 + 1)
+ .expect_err("over maximum")
+ .kind(),
+ MycStatusErrorKind::InvalidTime
+ );
+
+ let (publisher, reader) = myc_status_cache(
+ InstanceId::new("primary").expect("instance"),
+ observation(MycServicePhase::Ready, true, 5, 0),
+ )
+ .expect("status cache");
+ let rendered = format!("{publisher:?} {reader:?} {:?}", reader.snapshot());
+ for forbidden in [
+ SERVICE_REVISION,
+ LIB_REVISION,
+ "radroots.myc.config",
+ "aaaaaaaaaaaaaaaa",
+ "oldest_pending_at_utc",
+ ] {
+ assert!(!rendered.contains(forbidden));
+ }
+}
+
+#[test]
+fn model_boundaries_fail_closed_before_publication() {
+ assert_eq!(
+ MycStatusReasonCode::new("")
+ .expect_err("empty reason")
+ .kind(),
+ MycStatusErrorKind::InvalidReasonCode
+ );
+ assert_eq!(
+ MycStatusReasonCode::new("secret_canary_value")
+ .expect_err("unknown reason")
+ .kind(),
+ MycStatusErrorKind::InvalidReasonCode
+ );
+ let maximum = [
+ MycStatusReasonCode::IdentityUnavailable,
+ MycStatusReasonCode::DatabaseSchemaMismatch,
+ MycStatusReasonCode::DatabaseReadOnly,
+ MycStatusReasonCode::DatabaseLowDisk,
+ MycStatusReasonCode::RequiredRelayUnavailable,
+ MycStatusReasonCode::SubscriberNotActive,
+ MycStatusReasonCode::SignerProviderUnavailable,
+ MycStatusReasonCode::OutboxInvariantFailed,
+ MycStatusReasonCode::PublicationBacklogExceeded,
+ MycStatusReasonCode::AdminListenerFailed,
+ MycStatusReasonCode::OperationsListenerFailed,
+ MycStatusReasonCode::ShutdownInProgress,
+ ];
+ assert_eq!(
+ MycStatusReasonCodes::new(maximum)
+ .expect("maximum reasons")
+ .as_slice()
+ .len(),
+ 12
+ );
+ let mut infinite = std::iter::repeat(MycStatusReasonCode::IdentityUnavailable);
+ assert_eq!(
+ MycStatusReasonCodes::new(&mut infinite)
+ .expect_err("bounded infinite iterator")
+ .kind(),
+ MycStatusErrorKind::TooManyReasonCodes
+ );
+ assert_eq!(
+ infinite.next().expect("iterator retained"),
+ MycStatusReasonCode::IdentityUnavailable
+ );
+
+ assert_eq!(
+ MycStatusBuildInfoV1::new(
+ MycStatusBuildMode::Release,
+ Some("0.1.0"),
+ None,
+ Some(LIB_REVISION),
+ Some("1.97.1"),
+ Some("x86_64-unknown-linux-gnu"),
+ Some("service-host"),
+ )
+ .expect_err("release revision required")
+ .kind(),
+ MycStatusErrorKind::InvalidBuildInfo
+ );
+ assert_eq!(
+ MycStatusConfigurationIdentityV1::new(
+ "A".repeat(64),
+ MycStatusConfigurationSource::ExplicitConfig,
+ )
+ .expect_err("lowercase digest required")
+ .kind(),
+ MycStatusErrorKind::InvalidConfiguration
+ );
+ assert_eq!(
+ MycPersistenceStatusV1::new(
+ MycPersistenceHealthV1::Ready,
+ 0,
+ 0,
+ MycIntegrityStateV1::Verified,
+ MycStatusReasonCodes::empty(),
+ )
+ .expect_err("positive schema required")
+ .kind(),
+ MycStatusErrorKind::InvalidPersistence
+ );
+
+ let error = MycStatusCommonV1::new(
+ MycServicePhase::Ready,
+ false,
+ MycStatusReasonCodes::empty(),
+ 0,
+ build_info(),
+ MycStatusConfigurationIdentityV1::new(
+ "a".repeat(64),
+ MycStatusConfigurationSource::ExplicitConfig,
+ )
+ .expect("configuration"),
+ MycPersistenceStatusV1::new(
+ MycPersistenceHealthV1::Ready,
+ 9,
+ 0,
+ MycIntegrityStateV1::Verified,
+ MycStatusReasonCodes::empty(),
+ )
+ .expect("persistence"),
+ )
+ .expect_err("ready phase requires readiness");
+ assert_eq!(error.kind(), MycStatusErrorKind::InvalidLifecycle);
+}
+
+#[test]
+fn provider_aggregate_and_optional_oldest_time_are_deterministic() {
+ let ready = MycProviderStatusV1::new(
+ identity(true, true, &[]),
+ identity(true, true, &[]),
+ identity(false, false, &[]),
+ MycStatusReasonCodes::empty(),
+ )
+ .expect("ready provider");
+ assert_eq!(ready.health(), myc::MycProviderHealthV1::Ready);
+
+ let degraded = MycProviderStatusV1::new(
+ identity(true, true, &[]),
+ identity(true, true, &[]),
+ identity(true, false, &["identity_unavailable"]),
+ reasons(&["identity_unavailable"]),
+ )
+ .expect("degraded provider");
+ assert_eq!(degraded.health(), myc::MycProviderHealthV1::Degraded);
+
+ let unavailable = MycProviderStatusV1::new(
+ identity(true, false, &["identity_unavailable"]),
+ identity(true, true, &[]),
+ identity(false, false, &[]),
+ reasons(&["identity_unavailable"]),
+ )
+ .expect("unavailable provider");
+ assert_eq!(unavailable.health(), myc::MycProviderHealthV1::Unavailable);
+
+ let (_publisher, reader) = myc_status_cache(
+ InstanceId::new("primary").expect("instance"),
+ MycStatusObservationV1::new(
+ MycStatusCommonV1::new(
+ MycServicePhase::Ready,
+ true,
+ MycStatusReasonCodes::empty(),
+ 1,
+ build_info(),
+ MycStatusConfigurationIdentityV1::new(
+ "a".repeat(64),
+ MycStatusConfigurationSource::ExplicitConfig,
+ )
+ .expect("configuration"),
+ MycPersistenceStatusV1::new(
+ MycPersistenceHealthV1::Ready,
+ 9,
+ 0,
+ MycIntegrityStateV1::Verified,
+ MycStatusReasonCodes::empty(),
+ )
+ .expect("persistence"),
+ )
+ .expect("common status"),
+ ready,
+ MycRelayTransportStatusV1::new(
+ MycTransportHealthV1::Ready,
+ true,
+ 0,
+ MycStatusReasonCodes::empty(),
+ )
+ .expect("transport"),
+ MycConnectionCountsV1::default(),
+ MycOutboxStatusV1::new(0, 0, None),
+ ),
+ )
+ .expect("cache");
+ let wire = std::str::from_utf8(reader.snapshot().detailed_status_json())
+ .expect("status UTF-8")
+ .to_owned();
+ assert!(wire.contains("\"outbox\":{\"pending\":0,\"unknown\":0}"));
+ assert!(!wire.contains("oldest_pending_at_utc"));
+}