myc

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

runtime_foundation.rs (25860B)


      1 //! Existing-only, join-owned Myc runtime foundation.
      2 
      3 use core::fmt;
      4 use std::{error::Error, sync::mpsc};
      5 
      6 use radroots_service_host::{
      7     HostError, HostErrorKind, ShutdownPhase, TaskClassification, TaskMetadata, TaskName,
      8     TaskSupervisor,
      9 };
     10 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity};
     11 
     12 use crate::{
     13     MycConfigDocumentV1, MycDecryptedIdentity, MycLocalSignerClient, MycProviderBinding,
     14     MycProviderKind, MycProviderRole, MycRuntimeContext, MycStateHost, MycStateMetadata,
     15     open_myc_encrypted_identity, open_myc_state_read_write, resolve_myc_wrapping_credential,
     16 };
     17 
     18 #[cfg(test)]
     19 const RUNTIME_FOUNDATION_CONTRACT: &str =
     20     include_str!("../contracts/services_hardening/runtime_foundation.v1.json");
     21 
     22 /// Exact version of the Myc runtime-foundation contract.
     23 pub const MYC_RUNTIME_FOUNDATION_CONTRACT_VERSION: u32 = 1;
     24 
     25 /// Closed startup conditions that must all be satisfied before Myc is ready.
     26 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
     27 pub enum MycRuntimePrerequisite {
     28     ExistingState,
     29     TransportProvider,
     30     UserProvider,
     31     DiscoveryProvider,
     32     OutboxRecovery,
     33     RequiredRelayConnectivity,
     34     RequiredRelaySubscription,
     35     AdminListener,
     36     OperationsListener,
     37 }
     38 
     39 impl MycRuntimePrerequisite {
     40     /// Returns the exact machine-contract spelling.
     41     #[must_use]
     42     pub const fn as_str(self) -> &'static str {
     43         match self {
     44             Self::ExistingState => "existing_state",
     45             Self::TransportProvider => "transport_provider",
     46             Self::UserProvider => "user_provider",
     47             Self::DiscoveryProvider => "discovery_provider",
     48             Self::OutboxRecovery => "outbox_recovery",
     49             Self::RequiredRelayConnectivity => "required_relay_connectivity",
     50             Self::RequiredRelaySubscription => "required_relay_subscription",
     51             Self::AdminListener => "admin_listener",
     52             Self::OperationsListener => "operations_listener",
     53         }
     54     }
     55 
     56     const fn reason(self) -> MycRuntimeReadinessReason {
     57         match self {
     58             Self::ExistingState => MycRuntimeReadinessReason::DatabaseSchemaMismatch,
     59             Self::TransportProvider | Self::UserProvider | Self::DiscoveryProvider => {
     60                 MycRuntimeReadinessReason::SignerProviderUnavailable
     61             }
     62             Self::OutboxRecovery => MycRuntimeReadinessReason::OutboxInvariantFailed,
     63             Self::RequiredRelayConnectivity => MycRuntimeReadinessReason::RequiredRelayUnavailable,
     64             Self::RequiredRelaySubscription => MycRuntimeReadinessReason::SubscriberNotActive,
     65             Self::AdminListener => MycRuntimeReadinessReason::AdminListenerFailed,
     66             Self::OperationsListener => MycRuntimeReadinessReason::OperationsListenerFailed,
     67         }
     68     }
     69 }
     70 
     71 /// Closed stable reason vocabulary for an unsatisfied runtime prerequisite.
     72 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     73 pub enum MycRuntimeReadinessReason {
     74     AdminListenerFailed,
     75     DatabaseSchemaMismatch,
     76     OperationsListenerFailed,
     77     OutboxInvariantFailed,
     78     RequiredRelayUnavailable,
     79     SignerProviderUnavailable,
     80     SubscriberNotActive,
     81 }
     82 
     83 impl MycRuntimeReadinessReason {
     84     /// Returns the exact machine-contract spelling.
     85     #[must_use]
     86     pub const fn as_str(self) -> &'static str {
     87         match self {
     88             Self::AdminListenerFailed => "admin_listener_failed",
     89             Self::DatabaseSchemaMismatch => "database_schema_mismatch",
     90             Self::OperationsListenerFailed => "operations_listener_failed",
     91             Self::OutboxInvariantFailed => "outbox_invariant_failed",
     92             Self::RequiredRelayUnavailable => "required_relay_unavailable",
     93             Self::SignerProviderUnavailable => "signer_provider_unavailable",
     94             Self::SubscriberNotActive => "subscriber_not_active",
     95         }
     96     }
     97 }
     98 
     99 /// Immutable startup-readiness projection derived from admitted configuration.
    100 ///
    101 /// This snapshot is passive evidence. It performs no provider, relay, SQLite,
    102 /// DNS, listener, or filesystem probe when read.
    103 #[derive(Clone, PartialEq, Eq)]
    104 pub struct MycRuntimeReadiness {
    105     required: Box<[MycRuntimePrerequisite]>,
    106     satisfied: Box<[MycRuntimePrerequisite]>,
    107     reasons: Box<[MycRuntimeReadinessReason]>,
    108 }
    109 
    110 impl MycRuntimeReadiness {
    111     /// Returns readiness only when every exact prerequisite is satisfied.
    112     #[must_use]
    113     pub fn is_ready(&self) -> bool {
    114         self.required.len() == self.satisfied.len()
    115             && self
    116                 .required
    117                 .iter()
    118                 .all(|required| self.satisfied.contains(required))
    119     }
    120 
    121     /// Returns the exact ordered prerequisite inventory for this configuration.
    122     #[must_use]
    123     pub fn required(&self) -> &[MycRuntimePrerequisite] {
    124         &self.required
    125     }
    126 
    127     /// Returns the exact ordered prerequisites already proven at construction.
    128     #[must_use]
    129     pub fn satisfied(&self) -> &[MycRuntimePrerequisite] {
    130         &self.satisfied
    131     }
    132 
    133     /// Returns bounded stable reasons for every class of missing prerequisite.
    134     #[must_use]
    135     pub const fn reasons(&self) -> &[MycRuntimeReadinessReason] {
    136         &self.reasons
    137     }
    138 }
    139 
    140 impl fmt::Debug for MycRuntimeReadiness {
    141     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    142         formatter
    143             .debug_struct("MycRuntimeReadiness")
    144             .field("ready", &self.is_ready())
    145             .field("required", &self.required)
    146             .field("satisfied", &self.satisfied)
    147             .field("reasons", &self.reasons)
    148             .finish()
    149     }
    150 }
    151 
    152 /// Stable source-free runtime-foundation failure class.
    153 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    154 pub enum MycRuntimeFoundationErrorKind {
    155     InvalidBinding,
    156     StateOpen,
    157     Provider,
    158     TaskRegistration,
    159     TaskFailure,
    160     Readiness,
    161     Close,
    162 }
    163 
    164 impl MycRuntimeFoundationErrorKind {
    165     /// Returns the stable machine-facing safe code.
    166     #[must_use]
    167     pub const fn code(self) -> &'static str {
    168         match self {
    169             Self::InvalidBinding => "runtime_binding_invalid",
    170             Self::StateOpen => "runtime_state_open_failed",
    171             Self::Provider => "runtime_provider_unavailable",
    172             Self::TaskRegistration => "runtime_task_registration_failed",
    173             Self::TaskFailure => "runtime_task_failed",
    174             Self::Readiness => "runtime_readiness_invalid",
    175             Self::Close => "runtime_close_failed",
    176         }
    177     }
    178 
    179     const fn message(self) -> &'static str {
    180         match self {
    181             Self::InvalidBinding => "Myc runtime binding is invalid",
    182             Self::StateOpen => "Myc existing state could not be opened",
    183             Self::Provider => "Myc provider startup failed",
    184             Self::TaskRegistration => "Myc runtime task registration failed",
    185             Self::TaskFailure => "Myc supervised startup task failed",
    186             Self::Readiness => "Myc readiness prerequisites are invalid",
    187             Self::Close => "Myc runtime foundation could not close",
    188         }
    189     }
    190 }
    191 
    192 /// One redacted source-free runtime-foundation failure.
    193 #[derive(Clone, Copy, PartialEq, Eq)]
    194 pub struct MycRuntimeFoundationError {
    195     kind: MycRuntimeFoundationErrorKind,
    196 }
    197 
    198 impl MycRuntimeFoundationError {
    199     const fn new(kind: MycRuntimeFoundationErrorKind) -> Self {
    200         Self { kind }
    201     }
    202 
    203     /// Returns the stable failure kind.
    204     #[must_use]
    205     pub const fn kind(self) -> MycRuntimeFoundationErrorKind {
    206         self.kind
    207     }
    208 
    209     /// Returns the stable machine-facing safe code.
    210     #[must_use]
    211     pub const fn code(self) -> &'static str {
    212         self.kind.code()
    213     }
    214 }
    215 
    216 impl fmt::Debug for MycRuntimeFoundationError {
    217     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    218         formatter
    219             .debug_struct("MycRuntimeFoundationError")
    220             .field("kind", &self.kind)
    221             .finish()
    222     }
    223 }
    224 
    225 impl fmt::Display for MycRuntimeFoundationError {
    226     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    227         formatter.write_str(self.kind.message())
    228     }
    229 }
    230 
    231 impl Error for MycRuntimeFoundationError {}
    232 
    233 enum MycRuntimeProvider {
    234     EncryptedFile {
    235         role: MycProviderRole,
    236         _identity: MycDecryptedIdentity,
    237     },
    238     LocalSigner {
    239         role: MycProviderRole,
    240         _client: Box<MycLocalSignerClient>,
    241     },
    242 }
    243 
    244 enum ProviderStartupOutcome {
    245     Ready(MycDecryptedIdentity),
    246     ProviderFailure,
    247     TaskFailure,
    248 }
    249 
    250 impl MycRuntimeProvider {
    251     const fn role(&self) -> MycProviderRole {
    252         match self {
    253             Self::EncryptedFile { role, .. } | Self::LocalSigner { role, .. } => *role,
    254         }
    255     }
    256 
    257     const fn kind(&self) -> MycProviderKind {
    258         match self {
    259             Self::EncryptedFile { .. } => MycProviderKind::EncryptedFile,
    260             Self::LocalSigner { .. } => MycProviderKind::LocalSigner,
    261         }
    262     }
    263 }
    264 
    265 /// Existing-only Myc foundation with sealed state, provider, and task ownership.
    266 ///
    267 /// The final relay/admin/process task graph remains owned by later RCLDs. This
    268 /// value cannot be constructed directly or used to extract raw SQLite,
    269 /// provider-secret, task-handle, or cancellation authority.
    270 #[must_use = "the runtime foundation must be shut down so owned tasks and state are joined"]
    271 pub struct MycRuntimeFoundation {
    272     runtime: MycRuntimeContext,
    273     configuration: MycConfigDocumentV1,
    274     metadata: MycStateMetadata,
    275     state: MycStateHost,
    276     providers: Box<[MycRuntimeProvider]>,
    277     readiness: MycRuntimeReadiness,
    278     supervisor: TaskSupervisor,
    279 }
    280 
    281 impl MycRuntimeFoundation {
    282     /// Returns the immutable canonical instance context.
    283     #[must_use]
    284     pub const fn runtime_context(&self) -> &MycRuntimeContext {
    285         &self.runtime
    286     }
    287 
    288     /// Returns the admitted immutable configuration.
    289     #[must_use]
    290     pub const fn configuration(&self) -> &MycConfigDocumentV1 {
    291         &self.configuration
    292     }
    293 
    294     /// Returns metadata proven against the existing state host.
    295     #[must_use]
    296     pub const fn metadata(&self) -> &MycStateMetadata {
    297         &self.metadata
    298     }
    299 
    300     /// Returns the passive startup-readiness snapshot.
    301     #[must_use]
    302     pub const fn readiness(&self) -> &MycRuntimeReadiness {
    303         &self.readiness
    304     }
    305 
    306     /// Returns the configured retained provider kind for one enabled role.
    307     #[must_use]
    308     pub fn provider_kind(&self, role: MycProviderRole) -> Option<MycProviderKind> {
    309         self.providers
    310             .iter()
    311             .find(|provider| provider.role() == role)
    312             .map(MycRuntimeProvider::kind)
    313     }
    314 
    315     /// Requests cancellation, joins every owned task, and explicitly closes state.
    316     pub async fn shutdown(mut self) -> Result<(), MycRuntimeFoundationError> {
    317         self.supervisor.request_cancellation();
    318         let supervised = self.supervisor.supervise().await;
    319         let closed = self.state.close().await;
    320         if supervised.is_err() {
    321             Err(MycRuntimeFoundationError::new(
    322                 MycRuntimeFoundationErrorKind::TaskFailure,
    323             ))
    324         } else if closed.is_err() {
    325             Err(MycRuntimeFoundationError::new(
    326                 MycRuntimeFoundationErrorKind::Close,
    327             ))
    328         } else {
    329             Ok(())
    330         }
    331     }
    332 }
    333 
    334 impl fmt::Debug for MycRuntimeFoundation {
    335     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    336         formatter
    337             .debug_struct("MycRuntimeFoundation")
    338             .field("runtime", &"[redacted]")
    339             .field("configuration", &"[redacted]")
    340             .field("metadata", &"[redacted]")
    341             .field("state", &"[sealed]")
    342             .field("provider_count", &self.providers.len())
    343             .field("readiness", &self.readiness)
    344             .field("task_count", &self.supervisor.task_count())
    345             .finish()
    346     }
    347 }
    348 
    349 /// Opens an existing Myc state host and composes its sealed startup foundation.
    350 ///
    351 /// Missing state is never initialized. Encrypted-file providers are opened on
    352 /// bounded one-shot worker threads that are synchronously joined by
    353 /// `TaskSupervisor`; local-signer clients are constructed without I/O and
    354 /// remain unready until a later governed handshake succeeds. No task handle is
    355 /// detached or returned.
    356 pub async fn open_myc_runtime_foundation(
    357     runtime: MycRuntimeContext,
    358     configuration: MycConfigDocumentV1,
    359     metadata: MycStateMetadata,
    360     applied_at: MigrationAppliedAtUnixSeconds,
    361     build: &MigrationBuildIdentity,
    362 ) -> Result<MycRuntimeFoundation, MycRuntimeFoundationError> {
    363     if !metadata.matches_configuration(&runtime, &configuration) {
    364         return Err(MycRuntimeFoundationError::new(
    365             MycRuntimeFoundationErrorKind::InvalidBinding,
    366         ));
    367     }
    368     let state = open_myc_state_read_write(&runtime, &metadata, applied_at, build)
    369         .await
    370         .map_err(|_| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::StateOpen))?;
    371 
    372     match compose_after_state_open(runtime, configuration, metadata, state).await {
    373         Ok(foundation) => Ok(foundation),
    374         Err((error, state)) => {
    375             if state.close().await.is_err() {
    376                 Err(MycRuntimeFoundationError::new(
    377                     MycRuntimeFoundationErrorKind::Close,
    378                 ))
    379             } else {
    380                 Err(error)
    381             }
    382         }
    383     }
    384 }
    385 
    386 async fn compose_after_state_open(
    387     runtime: MycRuntimeContext,
    388     configuration: MycConfigDocumentV1,
    389     metadata: MycStateMetadata,
    390     state: MycStateHost,
    391 ) -> Result<MycRuntimeFoundation, (MycRuntimeFoundationError, MycStateHost)> {
    392     let (providers, readiness, supervisor) =
    393         match compose_runtime_components(&runtime, &configuration).await {
    394             Ok(components) => components,
    395             Err(error) => return Err((error, state)),
    396         };
    397     Ok(MycRuntimeFoundation {
    398         runtime,
    399         configuration,
    400         metadata,
    401         state,
    402         providers,
    403         readiness,
    404         supervisor,
    405     })
    406 }
    407 
    408 async fn compose_runtime_components(
    409     runtime: &MycRuntimeContext,
    410     configuration: &MycConfigDocumentV1,
    411 ) -> Result<
    412     (
    413         Box<[MycRuntimeProvider]>,
    414         MycRuntimeReadiness,
    415         TaskSupervisor,
    416     ),
    417     MycRuntimeFoundationError,
    418 > {
    419     let mut supervisor = TaskSupervisor::new();
    420     let mut providers: Vec<Option<MycRuntimeProvider>> = configuration
    421         .provider_contract()
    422         .bindings()
    423         .iter()
    424         .map(|_| None)
    425         .collect();
    426     let mut encrypted = Vec::new();
    427     let mut pending = Vec::new();
    428 
    429     for (index, binding) in configuration
    430         .provider_contract()
    431         .bindings()
    432         .iter()
    433         .cloned()
    434         .enumerate()
    435     {
    436         match binding.kind() {
    437             MycProviderKind::EncryptedFile => {
    438                 let role = binding.role();
    439                 let task = provider_task_metadata(role)?;
    440                 encrypted.push((index, role, binding, task));
    441             }
    442             MycProviderKind::LocalSigner => {
    443                 let role = binding.role();
    444                 let client = MycLocalSignerClient::new(&binding).map_err(|_| {
    445                     MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::Provider)
    446                 })?;
    447                 providers[index] = Some(MycRuntimeProvider::LocalSigner {
    448                     role,
    449                     _client: Box::new(client),
    450                 });
    451             }
    452         }
    453     }
    454 
    455     for (index, role, binding, task) in encrypted {
    456         let (sender, receiver) = mpsc::sync_channel(1);
    457         let task_runtime = runtime.clone();
    458         let registered = supervisor.spawn(task, move |_cancellation| async move {
    459             let outcome = match std::thread::Builder::new()
    460                 .name(provider_thread_name(role).to_owned())
    461                 .spawn(move || open_encrypted_provider(&task_runtime, &binding))
    462             {
    463                 Ok(thread) => match thread.join() {
    464                     Ok(Ok(identity)) => ProviderStartupOutcome::Ready(identity),
    465                     Ok(Err(())) => ProviderStartupOutcome::ProviderFailure,
    466                     Err(_) => ProviderStartupOutcome::TaskFailure,
    467                 },
    468                 Err(_) => ProviderStartupOutcome::TaskFailure,
    469             };
    470             let succeeded = matches!(outcome, ProviderStartupOutcome::Ready(_));
    471             let _ = sender.send(outcome);
    472             if succeeded {
    473                 Ok(())
    474             } else {
    475                 Err(HostError::new(HostErrorKind::TaskFailure))
    476             }
    477         });
    478         if registered.is_err() {
    479             supervisor.request_cancellation();
    480             let _ = supervisor.supervise().await;
    481             return Err(MycRuntimeFoundationError::new(
    482                 MycRuntimeFoundationErrorKind::TaskRegistration,
    483             ));
    484         }
    485         pending.push((index, role, receiver));
    486     }
    487 
    488     let supervised = supervisor.supervise().await;
    489     let mut provider_failure = false;
    490     let mut task_failure = false;
    491     for (index, role, receiver) in pending {
    492         match receiver.recv() {
    493             Ok(ProviderStartupOutcome::Ready(identity)) => {
    494                 providers[index] = Some(MycRuntimeProvider::EncryptedFile {
    495                     role,
    496                     _identity: identity,
    497                 });
    498             }
    499             Ok(ProviderStartupOutcome::ProviderFailure) => provider_failure = true,
    500             Ok(ProviderStartupOutcome::TaskFailure) | Err(_) => task_failure = true,
    501         }
    502     }
    503     if task_failure {
    504         return Err(MycRuntimeFoundationError::new(
    505             MycRuntimeFoundationErrorKind::TaskFailure,
    506         ));
    507     }
    508     if provider_failure {
    509         return Err(MycRuntimeFoundationError::new(
    510             MycRuntimeFoundationErrorKind::Provider,
    511         ));
    512     }
    513     if supervised.is_err() {
    514         return Err(MycRuntimeFoundationError::new(
    515             MycRuntimeFoundationErrorKind::TaskFailure,
    516         ));
    517     }
    518     let providers = providers
    519         .into_iter()
    520         .collect::<Option<Vec<_>>>()
    521         .ok_or_else(|| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::Provider))?
    522         .into_boxed_slice();
    523 
    524     let readiness = startup_readiness(configuration, &providers)?;
    525     let lifetime = TaskMetadata::new(
    526         TaskName::new("runtime_lifetime").map_err(|_| {
    527             MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration)
    528         })?,
    529         TaskClassification::Critical,
    530         Some(ShutdownPhase::RejectNewMutations),
    531     )
    532     .map_err(|_| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration))?;
    533     supervisor
    534         .spawn(lifetime, |cancellation| async move {
    535             cancellation.cancelled().await;
    536             Ok(())
    537         })
    538         .map_err(|_| {
    539             MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration)
    540         })?;
    541 
    542     Ok((providers, readiness, supervisor))
    543 }
    544 
    545 fn open_encrypted_provider(
    546     runtime: &MycRuntimeContext,
    547     binding: &MycProviderBinding,
    548 ) -> Result<MycDecryptedIdentity, ()> {
    549     let credential = resolve_myc_wrapping_credential(runtime, binding).map_err(|_| ())?;
    550     open_myc_encrypted_identity(binding, &credential).map_err(|_| ())
    551 }
    552 
    553 fn provider_task_metadata(
    554     role: MycProviderRole,
    555 ) -> Result<TaskMetadata, MycRuntimeFoundationError> {
    556     TaskMetadata::new(
    557         TaskName::new(provider_task_name(role)).map_err(|_| {
    558             MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration)
    559         })?,
    560         TaskClassification::OneShot,
    561         None,
    562     )
    563     .map_err(|_| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration))
    564 }
    565 
    566 const fn provider_task_name(role: MycProviderRole) -> &'static str {
    567     match role {
    568         MycProviderRole::Transport => "startup_transport_provider",
    569         MycProviderRole::User => "startup_user_provider",
    570         MycProviderRole::Discovery => "startup_discovery_provider",
    571     }
    572 }
    573 
    574 const fn provider_thread_name(role: MycProviderRole) -> &'static str {
    575     match role {
    576         MycProviderRole::Transport => "myc-provider-transport",
    577         MycProviderRole::User => "myc-provider-user",
    578         MycProviderRole::Discovery => "myc-provider-discovery",
    579     }
    580 }
    581 
    582 fn startup_readiness(
    583     configuration: &MycConfigDocumentV1,
    584     providers: &[MycRuntimeProvider],
    585 ) -> Result<MycRuntimeReadiness, MycRuntimeFoundationError> {
    586     let document = configuration.normalized();
    587     let mut required = vec![
    588         MycRuntimePrerequisite::ExistingState,
    589         MycRuntimePrerequisite::TransportProvider,
    590         MycRuntimePrerequisite::UserProvider,
    591     ];
    592     if configuration
    593         .provider_contract()
    594         .binding(MycProviderRole::Discovery)
    595         .is_some()
    596     {
    597         required.push(MycRuntimePrerequisite::DiscoveryProvider);
    598     }
    599     required.push(MycRuntimePrerequisite::OutboxRecovery);
    600 
    601     let relays = document
    602         .pointer("/relays")
    603         .and_then(serde_json::Value::as_array)
    604         .ok_or_else(readiness_error)?;
    605     if relays.iter().any(|relay| {
    606         relay
    607             .pointer("/required")
    608             .and_then(serde_json::Value::as_bool)
    609             == Some(true)
    610     }) {
    611         required.push(MycRuntimePrerequisite::RequiredRelayConnectivity);
    612     }
    613     if relays.iter().any(|relay| {
    614         relay
    615             .pointer("/required")
    616             .and_then(serde_json::Value::as_bool)
    617             == Some(true)
    618             && relay.pointer("/read").and_then(serde_json::Value::as_bool) == Some(true)
    619     }) {
    620         required.push(MycRuntimePrerequisite::RequiredRelaySubscription);
    621     }
    622     required.push(MycRuntimePrerequisite::AdminListener);
    623     if document
    624         .pointer("/operations/enabled")
    625         .and_then(serde_json::Value::as_bool)
    626         .ok_or_else(readiness_error)?
    627     {
    628         required.push(MycRuntimePrerequisite::OperationsListener);
    629     }
    630 
    631     let mut satisfied = vec![MycRuntimePrerequisite::ExistingState];
    632     for provider in providers {
    633         if provider.kind() == MycProviderKind::EncryptedFile {
    634             satisfied.push(provider_prerequisite(provider.role()));
    635         }
    636     }
    637     satisfied.sort_by_key(|prerequisite| {
    638         required
    639             .iter()
    640             .position(|candidate| candidate == prerequisite)
    641             .unwrap_or(usize::MAX)
    642     });
    643     let mut reasons = required
    644         .iter()
    645         .filter(|prerequisite| !satisfied.contains(prerequisite))
    646         .map(|prerequisite| prerequisite.reason())
    647         .collect::<Vec<_>>();
    648     reasons.sort_unstable();
    649     reasons.dedup();
    650     Ok(MycRuntimeReadiness {
    651         required: required.into_boxed_slice(),
    652         satisfied: satisfied.into_boxed_slice(),
    653         reasons: reasons.into_boxed_slice(),
    654     })
    655 }
    656 
    657 const fn provider_prerequisite(role: MycProviderRole) -> MycRuntimePrerequisite {
    658     match role {
    659         MycProviderRole::Transport => MycRuntimePrerequisite::TransportProvider,
    660         MycProviderRole::User => MycRuntimePrerequisite::UserProvider,
    661         MycProviderRole::Discovery => MycRuntimePrerequisite::DiscoveryProvider,
    662     }
    663 }
    664 
    665 const fn readiness_error() -> MycRuntimeFoundationError {
    666     MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::Readiness)
    667 }
    668 
    669 #[cfg(test)]
    670 mod tests {
    671     use super::*;
    672 
    673     #[test]
    674     fn machine_contract_and_closed_names_are_exact() {
    675         let contract: serde_json::Value =
    676             serde_json::from_str(RUNTIME_FOUNDATION_CONTRACT).expect("runtime contract");
    677         assert_eq!(contract["schema"], "radroots.myc.runtime-foundation");
    678         assert_eq!(contract["contract_version"], 1);
    679         let names = contract["readiness_prerequisites"]
    680             .as_array()
    681             .expect("prerequisite inventory")
    682             .iter()
    683             .map(|entry| entry["id"].as_str().expect("prerequisite id"))
    684             .collect::<Vec<_>>();
    685         let expected = [
    686             MycRuntimePrerequisite::ExistingState,
    687             MycRuntimePrerequisite::TransportProvider,
    688             MycRuntimePrerequisite::UserProvider,
    689             MycRuntimePrerequisite::DiscoveryProvider,
    690             MycRuntimePrerequisite::OutboxRecovery,
    691             MycRuntimePrerequisite::RequiredRelayConnectivity,
    692             MycRuntimePrerequisite::RequiredRelaySubscription,
    693             MycRuntimePrerequisite::AdminListener,
    694             MycRuntimePrerequisite::OperationsListener,
    695         ];
    696         assert_eq!(
    697             names,
    698             expected
    699                 .into_iter()
    700                 .map(MycRuntimePrerequisite::as_str)
    701                 .collect::<Vec<_>>()
    702         );
    703         let reasons = contract["readiness_prerequisites"]
    704             .as_array()
    705             .expect("prerequisite inventory")
    706             .iter()
    707             .map(|entry| entry["reason"].as_str().expect("prerequisite reason"))
    708             .collect::<Vec<_>>();
    709         assert_eq!(
    710             reasons,
    711             expected
    712                 .into_iter()
    713                 .map(MycRuntimePrerequisite::reason)
    714                 .map(MycRuntimeReadinessReason::as_str)
    715                 .collect::<Vec<_>>()
    716         );
    717     }
    718 
    719     #[test]
    720     fn safe_errors_cover_the_closed_inventory_without_sources() {
    721         for kind in [
    722             MycRuntimeFoundationErrorKind::InvalidBinding,
    723             MycRuntimeFoundationErrorKind::StateOpen,
    724             MycRuntimeFoundationErrorKind::Provider,
    725             MycRuntimeFoundationErrorKind::TaskRegistration,
    726             MycRuntimeFoundationErrorKind::TaskFailure,
    727             MycRuntimeFoundationErrorKind::Readiness,
    728             MycRuntimeFoundationErrorKind::Close,
    729         ] {
    730             let error = MycRuntimeFoundationError::new(kind);
    731             assert!(!error.code().is_empty());
    732             assert!(Error::source(&error).is_none());
    733             assert!(!format!("{error} {error:?}").contains("source"));
    734         }
    735     }
    736 }