myc

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

system_doctor.rs (8337B)


      1 //! Production active-doctor probes composed from existing sealed authorities.
      2 
      3 use radroots_service_host::{EntropySource, SystemEntropy, SystemWallClock, WallClock};
      4 use radroots_service_sqlite::{
      5     IntegrityCheckOutcome, IntegrityCheckedAtUnixMs, MinimumFreeBytes,
      6     PlatformStateFilesystemCapacitySource, inspect_state_filesystem_capacity,
      7 };
      8 
      9 use crate::admin_v1::admin_transport_limits;
     10 use crate::provider_executor::MycProviderExecutor;
     11 use crate::transport_nostr_adapter::MycNostrDeliveryAdapter;
     12 use crate::{
     13     MycConfigDocumentV1, MycDoctorCheckDefinition, MycDoctorCheckId, MycDoctorFuture,
     14     MycDoctorObservation, MycDoctorProbe, MycRuntimeContext, MycTaskCancellation,
     15     open_myc_state_inspection_from_config,
     16 };
     17 
     18 pub(crate) struct MycSystemDoctorProbe<'a> {
     19     runtime: &'a MycRuntimeContext,
     20     configuration: &'a MycConfigDocumentV1,
     21 }
     22 
     23 impl<'a> MycSystemDoctorProbe<'a> {
     24     pub(crate) const fn new(
     25         runtime: &'a MycRuntimeContext,
     26         configuration: &'a MycConfigDocumentV1,
     27     ) -> Self {
     28         Self {
     29             runtime,
     30             configuration,
     31         }
     32     }
     33 
     34     async fn run(&self, definition: MycDoctorCheckDefinition) -> bool {
     35         match definition.id() {
     36             MycDoctorCheckId::PathsPermissions => self.probe_paths(),
     37             MycDoctorCheckId::WriterLock
     38             | MycDoctorCheckId::SqliteSchema
     39             | MycDoctorCheckId::SqliteIntegrity
     40             | MycDoctorCheckId::OutboxInvariants => self.probe_state(definition.id()).await,
     41             MycDoctorCheckId::SqliteFreeSpace => self.probe_free_space(),
     42             MycDoctorCheckId::IdentityBinding => self.probe_identity_binding().await,
     43             MycDoctorCheckId::SignerProvider => self.probe_signer_provider().await,
     44             MycDoctorCheckId::AdminBindPolicy => self.probe_admin_policy(),
     45             MycDoctorCheckId::OperationsBindPolicy => self.probe_operations_policy(),
     46             MycDoctorCheckId::NetworkPolicy => {
     47                 MycNostrDeliveryAdapter::from_configuration(self.configuration).is_ok()
     48             }
     49             MycDoctorCheckId::RequiredRelays => {
     50                 let Some(deadline) = absolute_deadline(definition.deadline_ms()) else {
     51                     return false;
     52                 };
     53                 MycNostrDeliveryAdapter::probe_required_relays(self.configuration, deadline)
     54                     .await
     55                     .is_ok()
     56             }
     57             MycDoctorCheckId::ClockSkew => false,
     58         }
     59     }
     60 
     61     fn probe_paths(&self) -> bool {
     62         let Ok(paths) = crate::state_host::state_paths(self.runtime) else {
     63             return false;
     64         };
     65         let Ok(minimum) = MinimumFreeBytes::new(1) else {
     66             return false;
     67         };
     68         inspect_state_filesystem_capacity(&paths, minimum, &PlatformStateFilesystemCapacitySource)
     69             .is_ok()
     70     }
     71 
     72     async fn probe_state(&self, check: MycDoctorCheckId) -> bool {
     73         let Ok(state) =
     74             open_myc_state_inspection_from_config(self.runtime, self.configuration).await
     75         else {
     76             return false;
     77         };
     78         let outcome = match check {
     79             MycDoctorCheckId::WriterLock => true,
     80             MycDoctorCheckId::SqliteSchema => state.repository().verify_binding().await.is_ok(),
     81             MycDoctorCheckId::SqliteIntegrity => match integrity_time() {
     82                 Some(checked_at) => state
     83                     .inspect_integrity(checked_at)
     84                     .await
     85                     .is_ok_and(|report| {
     86                         report.sqlite() == IntegrityCheckOutcome::Verified
     87                             && report.foreign_keys() == IntegrityCheckOutcome::Verified
     88                     }),
     89                 None => false,
     90             },
     91             MycDoctorCheckId::OutboxInvariants => state
     92                 .repository()
     93                 .verify_delivery_invariants()
     94                 .await
     95                 .is_ok(),
     96             _ => false,
     97         };
     98         let closed = state.close().await.is_ok();
     99         outcome && closed
    100     }
    101 
    102     fn probe_free_space(&self) -> bool {
    103         let Some(minimum) = self
    104             .configuration
    105             .normalized()
    106             .pointer("/database/minimum_free_bytes")
    107             .and_then(serde_json::Value::as_u64)
    108             .and_then(|value| MinimumFreeBytes::new(value).ok())
    109         else {
    110             return false;
    111         };
    112         let Ok(paths) = crate::state_host::state_paths(self.runtime) else {
    113             return false;
    114         };
    115         inspect_state_filesystem_capacity(&paths, minimum, &PlatformStateFilesystemCapacitySource)
    116             .is_ok_and(|capacity| capacity.allows_authoritative_admission())
    117     }
    118 
    119     async fn probe_identity_binding(&self) -> bool {
    120         let cancellation = MycTaskCancellation::uncancelled();
    121         let Ok(executor) =
    122             MycProviderExecutor::open(self.runtime, self.configuration, &cancellation).await
    123         else {
    124             return false;
    125         };
    126         self.configuration
    127             .provider_contract()
    128             .bindings()
    129             .iter()
    130             .all(|binding| executor.contains_role(binding.role()))
    131     }
    132 
    133     async fn probe_signer_provider(&self) -> bool {
    134         let cancellation = MycTaskCancellation::uncancelled();
    135         let Ok(executor) =
    136             MycProviderExecutor::open(self.runtime, self.configuration, &cancellation).await
    137         else {
    138             return false;
    139         };
    140         let Some(observed_at) = wall_time_millis() else {
    141             return false;
    142         };
    143         let mut seed = [0_u8; 32];
    144         if SystemEntropy.fill_bytes(&mut seed).is_err() {
    145             return false;
    146         }
    147         executor
    148             .probe_all(observed_at, seed, &cancellation)
    149             .await
    150             .is_ok()
    151     }
    152 
    153     fn probe_admin_policy(&self) -> bool {
    154         let path = self.runtime.artifacts().admin_socket();
    155         path.is_absolute()
    156             && path.to_str().is_some_and(|value| value.len() <= 4_096)
    157             && admin_transport_limits(self.configuration).is_ok()
    158     }
    159 
    160     fn probe_operations_policy(&self) -> bool {
    161         let Some(enabled) = self
    162             .configuration
    163             .normalized()
    164             .pointer("/operations/enabled")
    165             .and_then(serde_json::Value::as_bool)
    166         else {
    167             return false;
    168         };
    169         !enabled
    170             || (self
    171                 .configuration
    172                 .normalized()
    173                 .pointer("/operations/listen")
    174                 .and_then(serde_json::Value::as_str)
    175                 .is_some()
    176                 && self
    177                     .configuration
    178                     .normalized()
    179                     .pointer("/operations/bind_policy")
    180                     .and_then(serde_json::Value::as_str)
    181                     .is_some())
    182     }
    183 }
    184 
    185 impl MycDoctorProbe for MycSystemDoctorProbe<'_> {
    186     fn probe(&self, definition: MycDoctorCheckDefinition) -> MycDoctorFuture<'_> {
    187         Box::pin(async move {
    188             if definition.id() == MycDoctorCheckId::ClockSkew {
    189                 MycDoctorObservation::Skipped
    190             } else if self.run(definition).await {
    191                 MycDoctorObservation::Pass
    192             } else {
    193                 MycDoctorObservation::Fail
    194             }
    195         })
    196     }
    197 }
    198 
    199 fn wall_time_millis() -> Option<u64> {
    200     SystemWallClock
    201         .now_utc()
    202         .ok()
    203         .and_then(|time| time.get().checked_mul(1_000))
    204         .filter(|value| i64::try_from(*value).is_ok())
    205 }
    206 
    207 fn absolute_deadline(duration_ms: u64) -> Option<u64> {
    208     wall_time_millis()?.checked_add(duration_ms)
    209 }
    210 
    211 fn integrity_time() -> Option<IntegrityCheckedAtUnixMs> {
    212     IntegrityCheckedAtUnixMs::new(wall_time_millis()?)
    213 }
    214 
    215 #[cfg(test)]
    216 mod tests {
    217     use super::*;
    218 
    219     #[test]
    220     fn deadline_math_is_checked_and_clock_skew_remains_unclaimed() {
    221         assert!(absolute_deadline(15_000).is_some());
    222         assert!(integrity_time().is_some());
    223     }
    224 
    225     #[test]
    226     fn production_probe_source_retains_no_raw_error_projection() {
    227         let source = include_str!("system_doctor.rs")
    228             .split("#[cfg(test)]")
    229             .next()
    230             .expect("production source");
    231         for forbidden in ["format!(\"{error", "to_string()", "source()"] {
    232             assert!(!source.contains(forbidden), "found `{forbidden}`");
    233         }
    234     }
    235 }