myc

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

status_v1.rs (36723B)


      1 //! Passive, latest-value Myc lifecycle and detailed-status publication.
      2 
      3 use core::fmt;
      4 use std::{error::Error, sync::Arc, time::Duration};
      5 
      6 use radroots_service_host::{
      7     BoundedMetricsSnapshot, BuildInfo as HostBuildInfo,
      8     BuildInfoEnvironment as HostBuildInfoEnvironment, BuildMode as HostBuildMode,
      9     CachedServiceState, CachedServiceStatePublisher, CachedServiceStateReader, CommonMetricGroup,
     10     ConfigurationIdentity as HostConfigurationIdentity,
     11     ConfigurationSource as HostConfigurationSource, ContractVersions as HostContractVersions,
     12     InstanceId, IntegrityState as HostIntegrityState, MetricDescriptor, MetricKind, MetricLabel,
     13     MetricLabelKey, MetricName, MetricSample, MetricValue,
     14     PersistenceHealth as HostPersistenceHealth, PersistenceSummary as HostPersistenceSummary,
     15     Readiness as HostReadiness, ReasonCode as HostReasonCode, ReasonCodes as HostReasonCodes,
     16     ServiceId, ServiceOperationalState as HostServiceOperationalState,
     17     ServicePhase as HostServicePhase, ServiceStatus, ServiceStatusDetail,
     18     Sha256Digest as HostSha256Digest, StatusContractError, StatusEncodingError, StatusModelError,
     19     UptimeMillis as HostUptimeMillis, cached_service_state,
     20 };
     21 use serde::Serialize;
     22 
     23 /// Exact version of the passive Myc status-cache contract.
     24 pub const MYC_STATUS_CACHE_CONTRACT_VERSION: u32 = 1;
     25 
     26 /// Maximum encoded byte length of one detailed Myc status response.
     27 pub const MYC_DETAILED_STATUS_MAX_UTF8_BYTES: usize =
     28     radroots_service_host::SERVICE_STATUS_MAX_UTF8_BYTES;
     29 
     30 /// Number of stable reason codes admitted by detailed Myc status.
     31 pub const MYC_STATUS_REASON_CODE_COUNT: usize = 12;
     32 
     33 /// One closed stable source-free status reason code.
     34 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
     35 #[serde(rename_all = "snake_case")]
     36 pub enum MycStatusReasonCode {
     37     IdentityUnavailable,
     38     DatabaseSchemaMismatch,
     39     DatabaseReadOnly,
     40     DatabaseLowDisk,
     41     RequiredRelayUnavailable,
     42     SubscriberNotActive,
     43     SignerProviderUnavailable,
     44     OutboxInvariantFailed,
     45     PublicationBacklogExceeded,
     46     AdminListenerFailed,
     47     OperationsListenerFailed,
     48     ShutdownInProgress,
     49 }
     50 
     51 impl MycStatusReasonCode {
     52     pub fn new(value: impl AsRef<str>) -> Result<Self, MycStatusError> {
     53         match value.as_ref() {
     54             "identity_unavailable" => Ok(Self::IdentityUnavailable),
     55             "database_schema_mismatch" => Ok(Self::DatabaseSchemaMismatch),
     56             "database_read_only" => Ok(Self::DatabaseReadOnly),
     57             "database_low_disk" => Ok(Self::DatabaseLowDisk),
     58             "required_relay_unavailable" => Ok(Self::RequiredRelayUnavailable),
     59             "subscriber_not_active" => Ok(Self::SubscriberNotActive),
     60             "signer_provider_unavailable" => Ok(Self::SignerProviderUnavailable),
     61             "outbox_invariant_failed" => Ok(Self::OutboxInvariantFailed),
     62             "publication_backlog_exceeded" => Ok(Self::PublicationBacklogExceeded),
     63             "admin_listener_failed" => Ok(Self::AdminListenerFailed),
     64             "operations_listener_failed" => Ok(Self::OperationsListenerFailed),
     65             "shutdown_in_progress" => Ok(Self::ShutdownInProgress),
     66             _ => Err(MycStatusError::new(MycStatusErrorKind::InvalidReasonCode)),
     67         }
     68     }
     69 
     70     #[must_use]
     71     pub const fn as_str(self) -> &'static str {
     72         match self {
     73             Self::IdentityUnavailable => "identity_unavailable",
     74             Self::DatabaseSchemaMismatch => "database_schema_mismatch",
     75             Self::DatabaseReadOnly => "database_read_only",
     76             Self::DatabaseLowDisk => "database_low_disk",
     77             Self::RequiredRelayUnavailable => "required_relay_unavailable",
     78             Self::SubscriberNotActive => "subscriber_not_active",
     79             Self::SignerProviderUnavailable => "signer_provider_unavailable",
     80             Self::OutboxInvariantFailed => "outbox_invariant_failed",
     81             Self::PublicationBacklogExceeded => "publication_backlog_exceeded",
     82             Self::AdminListenerFailed => "admin_listener_failed",
     83             Self::OperationsListenerFailed => "operations_listener_failed",
     84             Self::ShutdownInProgress => "shutdown_in_progress",
     85         }
     86     }
     87 }
     88 
     89 /// Canonically ordered, unique, bounded status reasons.
     90 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
     91 #[serde(transparent)]
     92 pub struct MycStatusReasonCodes(Vec<MycStatusReasonCode>);
     93 
     94 impl MycStatusReasonCodes {
     95     #[must_use]
     96     pub const fn empty() -> Self {
     97         Self(Vec::new())
     98     }
     99 
    100     pub fn new(
    101         values: impl IntoIterator<Item = MycStatusReasonCode>,
    102     ) -> Result<Self, MycStatusError> {
    103         let mut bounded = Vec::with_capacity(MYC_STATUS_REASON_CODE_COUNT);
    104         for value in values.into_iter().take(MYC_STATUS_REASON_CODE_COUNT + 1) {
    105             if bounded.len() == MYC_STATUS_REASON_CODE_COUNT {
    106                 return Err(MycStatusError::new(MycStatusErrorKind::TooManyReasonCodes));
    107             }
    108             bounded.push(value);
    109         }
    110         bounded.sort_unstable();
    111         bounded.dedup();
    112         Ok(Self(bounded))
    113     }
    114 
    115     #[must_use]
    116     pub fn as_slice(&self) -> &[MycStatusReasonCode] {
    117         &self.0
    118     }
    119 
    120     fn into_host(self) -> Result<HostReasonCodes, MycStatusError> {
    121         let values = self
    122             .0
    123             .into_iter()
    124             .map(|value| HostReasonCode::new(value.as_str()).map_err(map_contract_error))
    125             .collect::<Result<Vec<_>, _>>()?;
    126         HostReasonCodes::new(values).map_err(map_contract_error)
    127     }
    128 }
    129 
    130 /// Closed common lifecycle phase used by the Myc public boundary.
    131 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    132 #[serde(rename_all = "snake_case")]
    133 pub enum MycServicePhase {
    134     Starting,
    135     Ready,
    136     Degraded,
    137     Unready,
    138     Stopping,
    139     Failed,
    140 }
    141 
    142 impl MycServicePhase {
    143     const fn into_host(self) -> HostServicePhase {
    144         match self {
    145             Self::Starting => HostServicePhase::Starting,
    146             Self::Ready => HostServicePhase::Ready,
    147             Self::Degraded => HostServicePhase::Degraded,
    148             Self::Unready => HostServicePhase::Unready,
    149             Self::Stopping => HostServicePhase::Stopping,
    150             Self::Failed => HostServicePhase::Failed,
    151         }
    152     }
    153 
    154     const fn from_host(value: HostServicePhase) -> Self {
    155         match value {
    156             HostServicePhase::Starting => Self::Starting,
    157             HostServicePhase::Ready => Self::Ready,
    158             HostServicePhase::Degraded => Self::Degraded,
    159             HostServicePhase::Unready => Self::Unready,
    160             HostServicePhase::Stopping => Self::Stopping,
    161             HostServicePhase::Failed => Self::Failed,
    162         }
    163     }
    164 }
    165 
    166 /// Build-metadata admission mode for Myc status identity.
    167 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    168 pub enum MycStatusBuildMode {
    169     Development,
    170     Release,
    171 }
    172 
    173 /// Complete deterministic build identity retained behind the Myc boundary.
    174 pub struct MycStatusBuildInfoV1 {
    175     inner: HostBuildInfo,
    176 }
    177 
    178 impl MycStatusBuildInfoV1 {
    179     /// Validates the complete build/source-lock identity with fixed Myc contracts.
    180     pub fn new(
    181         mode: MycStatusBuildMode,
    182         service_version: Option<&str>,
    183         service_commit: Option<&str>,
    184         lib_revision: Option<&str>,
    185         rust_version: Option<&str>,
    186         target: Option<&str>,
    187         feature_profile: Option<&str>,
    188     ) -> Result<Self, MycStatusError> {
    189         let contract_versions =
    190             HostContractVersions::new(1, crate::MYC_STATE_SCHEMA_VERSION, 1, 1, 1)
    191                 .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidBuildInfo))?;
    192         HostBuildInfo::from_compile_time(
    193             match mode {
    194                 MycStatusBuildMode::Development => HostBuildMode::Development,
    195                 MycStatusBuildMode::Release => HostBuildMode::Release,
    196             },
    197             HostBuildInfoEnvironment {
    198                 service_version,
    199                 service_commit,
    200                 lib_revision,
    201                 rust_version,
    202                 target,
    203                 feature_profile,
    204                 contract_versions,
    205             },
    206         )
    207         .map(|inner| Self { inner })
    208         .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidBuildInfo))
    209     }
    210 }
    211 
    212 impl fmt::Debug for MycStatusBuildInfoV1 {
    213     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    214         formatter.write_str("MycStatusBuildInfoV1([redacted])")
    215     }
    216 }
    217 
    218 /// Exact configuration-source vocabulary exposed by detailed status.
    219 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    220 pub enum MycStatusConfigurationSource {
    221     ExplicitConfig,
    222     DerivedRepoLocal,
    223 }
    224 
    225 /// Safe configuration identity retained behind the Myc boundary.
    226 pub struct MycStatusConfigurationIdentityV1 {
    227     inner: HostConfigurationIdentity,
    228 }
    229 
    230 impl MycStatusConfigurationIdentityV1 {
    231     pub fn new(
    232         digest: impl AsRef<str>,
    233         source: MycStatusConfigurationSource,
    234     ) -> Result<Self, MycStatusError> {
    235         let service = ServiceId::new("myc")
    236             .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidConfiguration))?;
    237         let digest = HostSha256Digest::new(digest)
    238             .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidConfiguration))?;
    239         HostConfigurationIdentity::for_service(
    240             &service,
    241             digest,
    242             match source {
    243                 MycStatusConfigurationSource::ExplicitConfig => {
    244                     HostConfigurationSource::ExplicitConfig
    245                 }
    246                 MycStatusConfigurationSource::DerivedRepoLocal => {
    247                     HostConfigurationSource::DerivedRepoLocal
    248                 }
    249             },
    250         )
    251         .map(|inner| Self { inner })
    252         .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidConfiguration))
    253     }
    254 }
    255 
    256 impl fmt::Debug for MycStatusConfigurationIdentityV1 {
    257     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    258         formatter.write_str("MycStatusConfigurationIdentityV1([redacted])")
    259     }
    260 }
    261 
    262 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    263 pub enum MycPersistenceHealthV1 {
    264     Ready,
    265     ReadOnly,
    266     RepairRequired,
    267     Unavailable,
    268 }
    269 
    270 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    271 pub enum MycIntegrityStateV1 {
    272     Verified,
    273     VerificationRequired,
    274     Failed,
    275 }
    276 
    277 /// Validated persistence summary retained behind the Myc boundary.
    278 pub struct MycPersistenceStatusV1 {
    279     inner: HostPersistenceSummary,
    280 }
    281 
    282 impl MycPersistenceStatusV1 {
    283     pub fn new(
    284         health: MycPersistenceHealthV1,
    285         schema_version: u32,
    286         generation: u64,
    287         integrity: MycIntegrityStateV1,
    288         reason_codes: MycStatusReasonCodes,
    289     ) -> Result<Self, MycStatusError> {
    290         let reason_codes = reason_codes.into_host()?;
    291         HostPersistenceSummary::new(
    292             match health {
    293                 MycPersistenceHealthV1::Ready => HostPersistenceHealth::Ready,
    294                 MycPersistenceHealthV1::ReadOnly => HostPersistenceHealth::ReadOnly,
    295                 MycPersistenceHealthV1::RepairRequired => HostPersistenceHealth::RepairRequired,
    296                 MycPersistenceHealthV1::Unavailable => HostPersistenceHealth::Unavailable,
    297             },
    298             schema_version,
    299             generation,
    300             match integrity {
    301                 MycIntegrityStateV1::Verified => HostIntegrityState::Verified,
    302                 MycIntegrityStateV1::VerificationRequired => {
    303                     HostIntegrityState::VerificationRequired
    304                 }
    305                 MycIntegrityStateV1::Failed => HostIntegrityState::Failed,
    306             },
    307             reason_codes,
    308         )
    309         .map(|inner| Self { inner })
    310         .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidPersistence))
    311     }
    312 }
    313 
    314 impl fmt::Debug for MycPersistenceStatusV1 {
    315     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    316         formatter.write_str("MycPersistenceStatusV1([redacted])")
    317     }
    318 }
    319 
    320 /// One validated Unix timestamp used only for the oldest pending outbox item.
    321 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
    322 #[serde(transparent)]
    323 pub struct MycStatusUnixSeconds(u64);
    324 
    325 impl MycStatusUnixSeconds {
    326     /// Constructs a timestamp representable by SQLite and the frozen wire contract.
    327     pub fn new(value: u64) -> Result<Self, MycStatusError> {
    328         if value > i64::MAX as u64 {
    329             return Err(MycStatusError::new(MycStatusErrorKind::InvalidTime));
    330         }
    331         Ok(Self(value))
    332     }
    333 
    334     /// Returns exact whole Unix seconds.
    335     #[must_use]
    336     pub const fn get(self) -> u64 {
    337         self.0
    338     }
    339 }
    340 
    341 /// Passive availability of one configured Myc identity role.
    342 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    343 pub struct MycIdentityHealthV1 {
    344     configured: bool,
    345     available: bool,
    346     reason_codes: MycStatusReasonCodes,
    347 }
    348 
    349 impl MycIdentityHealthV1 {
    350     /// Constructs one role observation, rejecting availability without configuration.
    351     pub fn new(
    352         configured: bool,
    353         available: bool,
    354         reason_codes: MycStatusReasonCodes,
    355     ) -> Result<Self, MycStatusError> {
    356         if available && !configured {
    357             return Err(MycStatusError::new(
    358                 MycStatusErrorKind::InvalidIdentityHealth,
    359             ));
    360         }
    361         Ok(Self {
    362             configured,
    363             available,
    364             reason_codes,
    365         })
    366     }
    367 
    368     #[must_use]
    369     pub const fn is_configured(&self) -> bool {
    370         self.configured
    371     }
    372 
    373     #[must_use]
    374     pub const fn is_available(&self) -> bool {
    375         self.available
    376     }
    377 
    378     #[must_use]
    379     pub const fn reason_codes(&self) -> &MycStatusReasonCodes {
    380         &self.reason_codes
    381     }
    382 }
    383 
    384 /// Complete provider projection for the three fixed Myc identity roles.
    385 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    386 pub struct MycProviderStatusV1 {
    387     health: MycProviderHealthV1,
    388     transport: MycIdentityHealthV1,
    389     user: MycIdentityHealthV1,
    390     discovery: MycIdentityHealthV1,
    391     reason_codes: MycStatusReasonCodes,
    392 }
    393 
    394 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    395 #[serde(rename_all = "snake_case")]
    396 pub enum MycProviderHealthV1 {
    397     Ready,
    398     Degraded,
    399     Unavailable,
    400 }
    401 
    402 impl MycProviderStatusV1 {
    403     /// Derives aggregate provider health from the exact role inventory.
    404     pub fn new(
    405         transport: MycIdentityHealthV1,
    406         user: MycIdentityHealthV1,
    407         discovery: MycIdentityHealthV1,
    408         reason_codes: MycStatusReasonCodes,
    409     ) -> Result<Self, MycStatusError> {
    410         if !transport.configured || !user.configured {
    411             return Err(MycStatusError::new(
    412                 MycStatusErrorKind::InvalidProviderState,
    413             ));
    414         }
    415         let health = if !transport.available || !user.available {
    416             MycProviderHealthV1::Unavailable
    417         } else if discovery.configured && !discovery.available {
    418             MycProviderHealthV1::Degraded
    419         } else {
    420             MycProviderHealthV1::Ready
    421         };
    422         Ok(Self {
    423             health,
    424             transport,
    425             user,
    426             discovery,
    427             reason_codes,
    428         })
    429     }
    430 
    431     #[must_use]
    432     pub const fn health(&self) -> MycProviderHealthV1 {
    433         self.health
    434     }
    435 
    436     #[must_use]
    437     pub const fn transport(&self) -> &MycIdentityHealthV1 {
    438         &self.transport
    439     }
    440 
    441     #[must_use]
    442     pub const fn user(&self) -> &MycIdentityHealthV1 {
    443         &self.user
    444     }
    445 
    446     #[must_use]
    447     pub const fn discovery(&self) -> &MycIdentityHealthV1 {
    448         &self.discovery
    449     }
    450 
    451     #[must_use]
    452     pub const fn reason_codes(&self) -> &MycStatusReasonCodes {
    453         &self.reason_codes
    454     }
    455 }
    456 
    457 /// Passive relay-transport projection for detailed status.
    458 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    459 pub struct MycRelayTransportStatusV1 {
    460     health: MycTransportHealthV1,
    461     required_relays_ready: bool,
    462     connected_relay_count: u64,
    463     reason_codes: MycStatusReasonCodes,
    464 }
    465 
    466 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    467 #[serde(rename_all = "snake_case")]
    468 pub enum MycTransportHealthV1 {
    469     Ready,
    470     Degraded,
    471     Unavailable,
    472 }
    473 
    474 impl MycRelayTransportStatusV1 {
    475     /// Constructs one transport observation, rejecting contradictory ready state.
    476     pub fn new(
    477         health: MycTransportHealthV1,
    478         required_relays_ready: bool,
    479         connected_relay_count: u64,
    480         reason_codes: MycStatusReasonCodes,
    481     ) -> Result<Self, MycStatusError> {
    482         if health == MycTransportHealthV1::Ready && !required_relays_ready {
    483             return Err(MycStatusError::new(
    484                 MycStatusErrorKind::InvalidTransportState,
    485             ));
    486         }
    487         Ok(Self {
    488             health,
    489             required_relays_ready,
    490             connected_relay_count,
    491             reason_codes,
    492         })
    493     }
    494 
    495     #[must_use]
    496     pub const fn health(&self) -> MycTransportHealthV1 {
    497         self.health
    498     }
    499 
    500     #[must_use]
    501     pub const fn required_relays_ready(&self) -> bool {
    502         self.required_relays_ready
    503     }
    504 
    505     #[must_use]
    506     pub const fn connected_relay_count(&self) -> u64 {
    507         self.connected_relay_count
    508     }
    509 
    510     #[must_use]
    511     pub const fn reason_codes(&self) -> &MycStatusReasonCodes {
    512         &self.reason_codes
    513     }
    514 }
    515 
    516 /// Exact closed connection-count vocabulary rendered as the safe-count map.
    517 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
    518 pub struct MycConnectionCountsV1 {
    519     pending: u64,
    520     active: u64,
    521     denied: u64,
    522     expired: u64,
    523 }
    524 
    525 impl MycConnectionCountsV1 {
    526     #[must_use]
    527     pub const fn new(pending: u64, active: u64, denied: u64, expired: u64) -> Self {
    528         Self {
    529             pending,
    530             active,
    531             denied,
    532             expired,
    533         }
    534     }
    535 
    536     #[must_use]
    537     pub const fn pending(self) -> u64 {
    538         self.pending
    539     }
    540 
    541     #[must_use]
    542     pub const fn active(self) -> u64 {
    543         self.active
    544     }
    545 
    546     #[must_use]
    547     pub const fn denied(self) -> u64 {
    548         self.denied
    549     }
    550 
    551     #[must_use]
    552     pub const fn expired(self) -> u64 {
    553         self.expired
    554     }
    555 }
    556 
    557 /// Passive outbox summary derived by an owning runtime task before publication.
    558 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
    559 pub struct MycOutboxStatusV1 {
    560     pending: u64,
    561     unknown: u64,
    562     #[serde(skip_serializing_if = "Option::is_none")]
    563     oldest_pending_at_utc: Option<MycStatusUnixSeconds>,
    564 }
    565 
    566 impl MycOutboxStatusV1 {
    567     #[must_use]
    568     pub const fn new(
    569         pending: u64,
    570         unknown: u64,
    571         oldest_pending_at_utc: Option<MycStatusUnixSeconds>,
    572     ) -> Self {
    573         Self {
    574             pending,
    575             unknown,
    576             oldest_pending_at_utc,
    577         }
    578     }
    579 
    580     #[must_use]
    581     pub const fn pending(self) -> u64 {
    582         self.pending
    583     }
    584 
    585     #[must_use]
    586     pub const fn unknown(self) -> u64 {
    587         self.unknown
    588     }
    589 
    590     #[must_use]
    591     pub const fn oldest_pending_at_utc(self) -> Option<MycStatusUnixSeconds> {
    592         self.oldest_pending_at_utc
    593     }
    594 }
    595 
    596 #[derive(Serialize)]
    597 struct MycStatusDetailV1 {
    598     transport: MycIdentityHealthV1,
    599     user: MycIdentityHealthV1,
    600     discovery: MycIdentityHealthV1,
    601     connection_counts: MycConnectionCountsV1,
    602     outbox: MycOutboxStatusV1,
    603 }
    604 
    605 impl ServiceStatusDetail for MycStatusDetailV1 {
    606     type Provider = MycProviderStatusV1;
    607     type Transport = MycRelayTransportStatusV1;
    608 
    609     const FIELD_NAME: &'static str = "myc";
    610 }
    611 
    612 /// Validated common fields shared by one detailed status publication.
    613 ///
    614 /// Construction and publication perform validation and bounded encoding only.
    615 /// They do not query SQLite, providers, relays, DNS, credentials, or the clock.
    616 pub struct MycStatusCommonV1 {
    617     operational: HostServiceOperationalState,
    618     uptime: HostUptimeMillis,
    619     build: HostBuildInfo,
    620     configuration: HostConfigurationIdentity,
    621     persistence: HostPersistenceSummary,
    622 }
    623 
    624 impl MycStatusCommonV1 {
    625     /// Validates the common lifecycle and detailed-status envelope fields.
    626     pub fn new(
    627         phase: MycServicePhase,
    628         ready: bool,
    629         reason_codes: MycStatusReasonCodes,
    630         uptime_millis: u64,
    631         build: MycStatusBuildInfoV1,
    632         configuration: MycStatusConfigurationIdentityV1,
    633         persistence: MycPersistenceStatusV1,
    634     ) -> Result<Self, MycStatusError> {
    635         let operational = HostServiceOperationalState::new(
    636             phase.into_host(),
    637             if ready {
    638                 HostReadiness::READY
    639             } else {
    640                 HostReadiness::NOT_READY
    641             },
    642             reason_codes.into_host()?,
    643         )
    644         .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidLifecycle))?;
    645         let uptime = HostUptimeMillis::from_duration(Duration::from_millis(uptime_millis))
    646             .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidTime))?;
    647         Ok(Self {
    648             operational,
    649             uptime,
    650             build: build.inner,
    651             configuration: configuration.inner,
    652             persistence: persistence.inner,
    653         })
    654     }
    655 }
    656 
    657 impl fmt::Debug for MycStatusCommonV1 {
    658     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    659         formatter.write_str("MycStatusCommonV1([redacted])")
    660     }
    661 }
    662 
    663 /// One complete, already-observed status publication input.
    664 pub struct MycStatusObservationV1 {
    665     common: MycStatusCommonV1,
    666     provider: MycProviderStatusV1,
    667     transport: MycRelayTransportStatusV1,
    668     connection_counts: MycConnectionCountsV1,
    669     outbox: MycOutboxStatusV1,
    670 }
    671 
    672 impl MycStatusObservationV1 {
    673     #[must_use]
    674     pub fn new(
    675         common: MycStatusCommonV1,
    676         provider: MycProviderStatusV1,
    677         transport: MycRelayTransportStatusV1,
    678         connection_counts: MycConnectionCountsV1,
    679         outbox: MycOutboxStatusV1,
    680     ) -> Self {
    681         Self {
    682             common,
    683             provider,
    684             transport,
    685             connection_counts,
    686             outbox,
    687         }
    688     }
    689 
    690     fn into_cached(self, instance: &InstanceId) -> Result<PreparedMycStatus, MycStatusError> {
    691         let operational = self.common.operational.clone();
    692         let operations_metrics = bounded_operations_metrics(&operational)?;
    693         let detail = MycStatusDetailV1 {
    694             transport: self.provider.transport.clone(),
    695             user: self.provider.user.clone(),
    696             discovery: self.provider.discovery.clone(),
    697             connection_counts: self.connection_counts,
    698             outbox: self.outbox,
    699         };
    700         let service = ServiceId::new("myc")
    701             .map_err(|_| MycStatusError::new(MycStatusErrorKind::InvalidModel))?;
    702         let status = ServiceStatus::new(
    703             service,
    704             instance.clone(),
    705             self.common.operational,
    706             self.common.uptime,
    707             self.common.build,
    708             self.common.configuration,
    709             self.common.persistence,
    710             self.provider,
    711             self.transport,
    712             detail,
    713         )
    714         .map_err(map_model_error)?;
    715         let json = status.to_bounded_json().map_err(map_encoding_error)?;
    716         Ok(PreparedMycStatus {
    717             detail: CachedServiceState::new(
    718                 operational.clone(),
    719                 MycCachedStatus {
    720                     json: json.into_boxed_slice(),
    721                 },
    722             ),
    723             operations: CachedServiceState::new(operational, operations_metrics),
    724         })
    725     }
    726 }
    727 
    728 impl fmt::Debug for MycStatusObservationV1 {
    729     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    730         formatter.write_str("MycStatusObservationV1([redacted])")
    731     }
    732 }
    733 
    734 struct MycCachedStatus {
    735     json: Box<[u8]>,
    736 }
    737 
    738 struct PreparedMycStatus {
    739     detail: CachedServiceState<MycCachedStatus>,
    740     operations: CachedServiceState<BoundedMetricsSnapshot>,
    741 }
    742 
    743 fn bounded_operations_metrics(
    744     operational: &HostServiceOperationalState,
    745 ) -> Result<BoundedMetricsSnapshot, MycStatusError> {
    746     let phase_name = MetricName::new("radroots_myc_service_phase").map_err(map_metrics_error)?;
    747     let ready_name = MetricName::new("radroots_myc_service_ready").map_err(map_metrics_error)?;
    748     let descriptors = [
    749         MetricDescriptor::new(
    750             CommonMetricGroup::Phase,
    751             phase_name.clone(),
    752             "Current cached Myc service phase.",
    753             MetricKind::Gauge,
    754             [MetricLabelKey::Phase],
    755         )
    756         .map_err(map_metrics_error)?,
    757         MetricDescriptor::new(
    758             CommonMetricGroup::Phase,
    759             ready_name.clone(),
    760             "Current cached Myc readiness bit.",
    761             MetricKind::Gauge,
    762             [],
    763         )
    764         .map_err(map_metrics_error)?,
    765     ];
    766     let samples = [
    767         MetricSample::new(
    768             phase_name,
    769             MetricValue::Gauge(1),
    770             [MetricLabel::phase(operational.phase())],
    771         )
    772         .map_err(map_metrics_error)?,
    773         MetricSample::new(
    774             ready_name,
    775             MetricValue::Gauge(i64::from(operational.readiness().is_ready())),
    776             [],
    777         )
    778         .map_err(map_metrics_error)?,
    779     ];
    780     BoundedMetricsSnapshot::new(descriptors, samples).map_err(map_metrics_error)
    781 }
    782 
    783 fn map_metrics_error(_: radroots_service_host::MetricsContractError) -> MycStatusError {
    784     MycStatusError::new(MycStatusErrorKind::InvalidModel)
    785 }
    786 
    787 impl fmt::Debug for MycCachedStatus {
    788     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    789         formatter
    790             .debug_struct("MycCachedStatus")
    791             .field("json_utf8_bytes", &self.json.len())
    792             .finish()
    793     }
    794 }
    795 
    796 /// Sole publication authority for one process-local Myc status cache.
    797 ///
    798 /// This type deliberately does not implement `Clone`. A successful publish
    799 /// atomically replaces the one retained snapshot. A failed encoding or illegal
    800 /// lifecycle transition leaves the previous snapshot unchanged.
    801 pub struct MycStatusPublisher {
    802     instance: InstanceId,
    803     inner: CachedServiceStatePublisher<MycCachedStatus>,
    804     operations: CachedServiceStatePublisher<BoundedMetricsSnapshot>,
    805 }
    806 
    807 impl MycStatusPublisher {
    808     /// Encodes one observation, publishes its passive operations projection,
    809     /// and then atomically replaces the detailed-status snapshot.
    810     pub fn publish(&mut self, next: MycStatusObservationV1) -> Result<(), MycStatusError> {
    811         let next = next.into_cached(&self.instance)?;
    812         self.operations
    813             .publish(next.operations)
    814             .map_err(map_contract_error)?;
    815         self.inner.publish(next.detail).map_err(map_contract_error)
    816     }
    817 
    818     /// Creates another passive reader without sharing publication authority.
    819     #[must_use]
    820     pub fn subscribe(&self) -> MycStatusReader {
    821         MycStatusReader {
    822             inner: self.inner.subscribe(),
    823             operations: self.operations.subscribe(),
    824         }
    825     }
    826 }
    827 
    828 impl fmt::Debug for MycStatusPublisher {
    829     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    830         formatter.write_str("MycStatusPublisher([sealed])")
    831     }
    832 }
    833 
    834 /// Cloneable passive reader of the latest Myc lifecycle and detailed status.
    835 pub struct MycStatusReader {
    836     inner: CachedServiceStateReader<MycCachedStatus>,
    837     operations: CachedServiceStateReader<BoundedMetricsSnapshot>,
    838 }
    839 
    840 impl Clone for MycStatusReader {
    841     fn clone(&self) -> Self {
    842         Self {
    843             inner: self.inner.clone(),
    844             operations: self.operations.clone(),
    845         }
    846     }
    847 }
    848 
    849 impl MycStatusReader {
    850     /// Returns the latest immutable snapshot without awaiting or probing.
    851     #[must_use]
    852     pub fn snapshot(&self) -> MycStatusSnapshot {
    853         MycStatusSnapshot {
    854             inner: self.inner.snapshot(),
    855         }
    856     }
    857 
    858     /// Waits for a later publication and returns the newest retained value.
    859     pub async fn changed(&mut self) -> Result<MycStatusSnapshot, MycStatusError> {
    860         self.inner
    861             .changed()
    862             .await
    863             .map(|inner| MycStatusSnapshot { inner })
    864             .map_err(|_| MycStatusError::new(MycStatusErrorKind::PublisherDropped))
    865     }
    866 
    867     pub(crate) fn operations_cache(&self) -> CachedServiceStateReader<BoundedMetricsSnapshot> {
    868         self.operations.clone()
    869     }
    870 }
    871 
    872 impl fmt::Debug for MycStatusReader {
    873     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    874         formatter.write_str("MycStatusReader([passive])")
    875     }
    876 }
    877 
    878 /// One immutable point-in-time status snapshot backed by the retained cache `Arc`.
    879 pub struct MycStatusSnapshot {
    880     inner: Arc<CachedServiceState<MycCachedStatus>>,
    881 }
    882 
    883 impl MycStatusSnapshot {
    884     #[must_use]
    885     pub fn phase(&self) -> MycServicePhase {
    886         MycServicePhase::from_host(self.inner.operational().phase())
    887     }
    888 
    889     #[must_use]
    890     pub fn is_ready(&self) -> bool {
    891         self.inner.operational().readiness().is_ready()
    892     }
    893 
    894     /// Returns the already-bounded canonical detailed-status JSON bytes.
    895     #[must_use]
    896     pub fn detailed_status_json(&self) -> &[u8] {
    897         &self.inner.metrics().json
    898     }
    899 }
    900 
    901 impl fmt::Debug for MycStatusSnapshot {
    902     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    903         formatter
    904             .debug_struct("MycStatusSnapshot")
    905             .field("phase", &self.phase())
    906             .field("ready", &self.is_ready())
    907             .field("json_utf8_bytes", &self.detailed_status_json().len())
    908             .finish()
    909     }
    910 }
    911 
    912 /// Creates the single-writer, one-latest-value Myc status cache.
    913 pub fn myc_status_cache(
    914     instance: InstanceId,
    915     initial: MycStatusObservationV1,
    916 ) -> Result<(MycStatusPublisher, MycStatusReader), MycStatusError> {
    917     let initial = initial.into_cached(&instance)?;
    918     let (inner, reader) = cached_service_state(initial.detail);
    919     let (operations, operations_reader) = cached_service_state(initial.operations);
    920     Ok((
    921         MycStatusPublisher {
    922             instance,
    923             inner,
    924             operations,
    925         },
    926         MycStatusReader {
    927             inner: reader,
    928             operations: operations_reader,
    929         },
    930     ))
    931 }
    932 
    933 /// Stable source-free status failure category.
    934 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    935 pub enum MycStatusErrorKind {
    936     InvalidReasonCode,
    937     TooManyReasonCodes,
    938     InvalidLifecycle,
    939     InvalidBuildInfo,
    940     InvalidConfiguration,
    941     InvalidPersistence,
    942     InvalidIdentityHealth,
    943     InvalidProviderState,
    944     InvalidTransportState,
    945     InvalidTime,
    946     InvalidModel,
    947     Encoding,
    948     ResponseTooLarge,
    949     InvalidTransition,
    950     PublisherDropped,
    951 }
    952 
    953 impl MycStatusErrorKind {
    954     #[must_use]
    955     pub const fn code(self) -> &'static str {
    956         match self {
    957             Self::InvalidReasonCode => "status_reason_code_invalid",
    958             Self::TooManyReasonCodes => "status_reason_count_exceeded",
    959             Self::InvalidLifecycle => "status_lifecycle_invalid",
    960             Self::InvalidBuildInfo => "status_build_info_invalid",
    961             Self::InvalidConfiguration => "status_configuration_invalid",
    962             Self::InvalidPersistence => "status_persistence_invalid",
    963             Self::InvalidIdentityHealth => "status_identity_health_invalid",
    964             Self::InvalidProviderState => "status_provider_state_invalid",
    965             Self::InvalidTransportState => "status_transport_state_invalid",
    966             Self::InvalidTime => "status_time_invalid",
    967             Self::InvalidModel => "status_model_invalid",
    968             Self::Encoding => "status_encoding_failed",
    969             Self::ResponseTooLarge => "status_response_too_large",
    970             Self::InvalidTransition => "status_transition_invalid",
    971             Self::PublisherDropped => "status_publisher_dropped",
    972         }
    973     }
    974 
    975     const fn message(self) -> &'static str {
    976         match self {
    977             Self::InvalidReasonCode => "Myc status reason code is invalid",
    978             Self::TooManyReasonCodes => "Myc status has too many reason codes",
    979             Self::InvalidLifecycle => "Myc lifecycle status is invalid",
    980             Self::InvalidBuildInfo => "Myc status build identity is invalid",
    981             Self::InvalidConfiguration => "Myc status configuration identity is invalid",
    982             Self::InvalidPersistence => "Myc persistence status is invalid",
    983             Self::InvalidIdentityHealth => "Myc identity health is invalid",
    984             Self::InvalidProviderState => "Myc provider status is invalid",
    985             Self::InvalidTransportState => "Myc transport status is invalid",
    986             Self::InvalidTime => "Myc status time is invalid",
    987             Self::InvalidModel => "Myc detailed status is invalid",
    988             Self::Encoding => "Myc detailed status encoding failed",
    989             Self::ResponseTooLarge => "Myc detailed status exceeds its byte limit",
    990             Self::InvalidTransition => "Myc lifecycle transition is invalid",
    991             Self::PublisherDropped => "Myc status publisher is unavailable",
    992         }
    993     }
    994 }
    995 
    996 /// One redacted source-free status failure.
    997 #[derive(Clone, Copy, PartialEq, Eq)]
    998 pub struct MycStatusError {
    999     kind: MycStatusErrorKind,
   1000 }
   1001 
   1002 impl MycStatusError {
   1003     const fn new(kind: MycStatusErrorKind) -> Self {
   1004         Self { kind }
   1005     }
   1006 
   1007     #[must_use]
   1008     pub const fn kind(self) -> MycStatusErrorKind {
   1009         self.kind
   1010     }
   1011 
   1012     #[must_use]
   1013     pub const fn code(self) -> &'static str {
   1014         self.kind.code()
   1015     }
   1016 }
   1017 
   1018 impl fmt::Debug for MycStatusError {
   1019     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1020         formatter
   1021             .debug_struct("MycStatusError")
   1022             .field("kind", &self.kind)
   1023             .finish()
   1024     }
   1025 }
   1026 
   1027 impl fmt::Display for MycStatusError {
   1028     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1029         formatter.write_str(self.kind.message())
   1030     }
   1031 }
   1032 
   1033 impl Error for MycStatusError {}
   1034 
   1035 const fn map_model_error(_error: StatusModelError) -> MycStatusError {
   1036     MycStatusError::new(MycStatusErrorKind::InvalidModel)
   1037 }
   1038 
   1039 const fn map_encoding_error(error: StatusEncodingError) -> MycStatusError {
   1040     match error {
   1041         StatusEncodingError::EncodingFailed => MycStatusError::new(MycStatusErrorKind::Encoding),
   1042         StatusEncodingError::ResponseTooLarge => {
   1043             MycStatusError::new(MycStatusErrorKind::ResponseTooLarge)
   1044         }
   1045     }
   1046 }
   1047 
   1048 const fn map_contract_error(_error: StatusContractError) -> MycStatusError {
   1049     MycStatusError::new(MycStatusErrorKind::InvalidTransition)
   1050 }
   1051 
   1052 #[cfg(test)]
   1053 mod tests {
   1054     use super::*;
   1055 
   1056     #[test]
   1057     fn invalid_status_inputs_fail_with_safe_source_free_errors() {
   1058         assert_eq!(
   1059             MycIdentityHealthV1::new(false, true, MycStatusReasonCodes::empty()),
   1060             Err(MycStatusError::new(
   1061                 MycStatusErrorKind::InvalidIdentityHealth
   1062             ))
   1063         );
   1064         let unavailable =
   1065             MycIdentityHealthV1::new(true, false, MycStatusReasonCodes::empty()).unwrap();
   1066         assert_eq!(
   1067             MycProviderStatusV1::new(
   1068                 MycIdentityHealthV1::new(false, false, MycStatusReasonCodes::empty()).unwrap(),
   1069                 unavailable,
   1070                 MycIdentityHealthV1::new(false, false, MycStatusReasonCodes::empty()).unwrap(),
   1071                 MycStatusReasonCodes::empty(),
   1072             ),
   1073             Err(MycStatusError::new(
   1074                 MycStatusErrorKind::InvalidProviderState
   1075             ))
   1076         );
   1077         assert_eq!(
   1078             MycRelayTransportStatusV1::new(
   1079                 MycTransportHealthV1::Ready,
   1080                 false,
   1081                 0,
   1082                 MycStatusReasonCodes::empty(),
   1083             ),
   1084             Err(MycStatusError::new(
   1085                 MycStatusErrorKind::InvalidTransportState
   1086             ))
   1087         );
   1088         assert_eq!(
   1089             MycStatusUnixSeconds::new(i64::MAX as u64 + 1),
   1090             Err(MycStatusError::new(MycStatusErrorKind::InvalidTime))
   1091         );
   1092         for kind in [
   1093             MycStatusErrorKind::InvalidReasonCode,
   1094             MycStatusErrorKind::TooManyReasonCodes,
   1095             MycStatusErrorKind::InvalidLifecycle,
   1096             MycStatusErrorKind::InvalidBuildInfo,
   1097             MycStatusErrorKind::InvalidConfiguration,
   1098             MycStatusErrorKind::InvalidPersistence,
   1099             MycStatusErrorKind::InvalidIdentityHealth,
   1100             MycStatusErrorKind::InvalidProviderState,
   1101             MycStatusErrorKind::InvalidTransportState,
   1102             MycStatusErrorKind::InvalidTime,
   1103             MycStatusErrorKind::InvalidModel,
   1104             MycStatusErrorKind::Encoding,
   1105             MycStatusErrorKind::ResponseTooLarge,
   1106             MycStatusErrorKind::InvalidTransition,
   1107             MycStatusErrorKind::PublisherDropped,
   1108         ] {
   1109             let error = MycStatusError::new(kind);
   1110             assert!(!error.code().is_empty());
   1111             assert!(Error::source(&error).is_none());
   1112             assert!(!format!("{error} {error:?}").contains("source"));
   1113         }
   1114     }
   1115 }