rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

runtime_adapters.rs (30598B)


      1 //! Injected, bounded runtime capability composition.
      2 
      3 use core::{fmt, time::Duration};
      4 use std::{error::Error, sync::Arc};
      5 
      6 use radroots_service_host::{
      7     EntropySource, MonotonicClock, MonotonicDeadline, MonotonicTime, SystemEntropy,
      8     SystemMonotonicClock, SystemWallClock, TaskSupervisor, UnixTimeSeconds, WallClock,
      9 };
     10 use radroots_transport::{EventSink, EventSource, EventSubscriber};
     11 
     12 use crate::{
     13     RhiCredentialResolutionError, RhiDecryptedIdentity, RhiEncryptedIdentityEnvelopeError,
     14     RhiIdentityEnvelopeBinding, RhiRuntimeContext, RhiWrappingCredential,
     15     open_rhi_encrypted_identity, resolve_rhi_wrapping_credential,
     16 };
     17 
     18 #[cfg(test)]
     19 const RUNTIME_ADAPTER_CONTRACT: &str =
     20     include_str!("../contracts/services_hardening/runtime_adapters.v1.json");
     21 
     22 /// Exact version of the RHI runtime-adapter contract.
     23 pub const RHI_RUNTIME_ADAPTER_CONTRACT_VERSION: u32 = 1;
     24 
     25 /// Largest full-jitter ceiling admitted by the RHI v1 configuration contract.
     26 pub const RHI_RUNTIME_JITTER_MAX_MILLISECONDS: u64 = 3_600_000;
     27 
     28 /// Maximum entropy draws allowed for one exact unbiased full-jitter sample.
     29 pub const RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS: usize = 16;
     30 
     31 /// Stable source-free runtime-adapter failure classification.
     32 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     33 pub enum RhiRuntimeAdapterErrorKind {
     34     InvalidJitterBound,
     35     EntropyUnavailable,
     36     WallClockUnavailable,
     37     MonotonicDeadlineInvalid,
     38     CredentialAccess,
     39     IdentityAccess,
     40 }
     41 
     42 impl RhiRuntimeAdapterErrorKind {
     43     /// Returns the stable machine-facing safe code.
     44     #[must_use]
     45     pub const fn code(self) -> &'static str {
     46         match self {
     47             Self::InvalidJitterBound => "runtime_jitter_bound_invalid",
     48             Self::EntropyUnavailable => "runtime_entropy_unavailable",
     49             Self::WallClockUnavailable => "runtime_wall_clock_unavailable",
     50             Self::MonotonicDeadlineInvalid => "runtime_monotonic_deadline_invalid",
     51             Self::CredentialAccess => "runtime_credential_access_failed",
     52             Self::IdentityAccess => "runtime_identity_access_failed",
     53         }
     54     }
     55 
     56     const fn message(self) -> &'static str {
     57         match self {
     58             Self::InvalidJitterBound => "RHI jitter bound is invalid",
     59             Self::EntropyUnavailable => "RHI entropy source is unavailable",
     60             Self::WallClockUnavailable => "RHI wall clock is unavailable",
     61             Self::MonotonicDeadlineInvalid => "RHI monotonic deadline is invalid",
     62             Self::CredentialAccess => "RHI credential access failed",
     63             Self::IdentityAccess => "RHI identity access failed",
     64         }
     65     }
     66 }
     67 
     68 /// One redacted source-free runtime-adapter failure.
     69 #[derive(Clone, Copy, PartialEq, Eq)]
     70 pub struct RhiRuntimeAdapterError {
     71     kind: RhiRuntimeAdapterErrorKind,
     72 }
     73 
     74 impl RhiRuntimeAdapterError {
     75     const fn new(kind: RhiRuntimeAdapterErrorKind) -> Self {
     76         Self { kind }
     77     }
     78 
     79     /// Returns the stable failure kind.
     80     #[must_use]
     81     pub const fn kind(self) -> RhiRuntimeAdapterErrorKind {
     82         self.kind
     83     }
     84 
     85     /// Returns the stable machine-facing safe code.
     86     #[must_use]
     87     pub const fn code(self) -> &'static str {
     88         self.kind.code()
     89     }
     90 }
     91 
     92 impl fmt::Debug for RhiRuntimeAdapterError {
     93     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     94         formatter
     95             .debug_struct("RhiRuntimeAdapterError")
     96             .field("kind", &self.kind)
     97             .finish()
     98     }
     99 }
    100 
    101 impl fmt::Display for RhiRuntimeAdapterError {
    102     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    103         formatter.write_str(self.kind.message())
    104     }
    105 }
    106 
    107 impl Error for RhiRuntimeAdapterError {}
    108 
    109 /// Validated inclusive maximum for one full-jitter sample, in whole milliseconds.
    110 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    111 pub struct RhiJitterBoundMilliseconds(u64);
    112 
    113 impl RhiJitterBoundMilliseconds {
    114     /// Validates a whole-millisecond bound against the complete RHI v1 ceiling.
    115     pub const fn new(milliseconds: u64) -> Result<Self, RhiRuntimeAdapterError> {
    116         if milliseconds > RHI_RUNTIME_JITTER_MAX_MILLISECONDS {
    117             Err(RhiRuntimeAdapterError::new(
    118                 RhiRuntimeAdapterErrorKind::InvalidJitterBound,
    119             ))
    120         } else {
    121             Ok(Self(milliseconds))
    122         }
    123     }
    124 
    125     /// Returns the inclusive maximum in whole milliseconds.
    126     #[must_use]
    127     pub const fn get(self) -> u64 {
    128         self.0
    129     }
    130 }
    131 
    132 /// One injected full-jitter result, in whole milliseconds.
    133 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    134 pub struct RhiJitterMilliseconds(u64);
    135 
    136 impl RhiJitterMilliseconds {
    137     /// Returns the sampled value.
    138     #[must_use]
    139     pub const fn get(self) -> u64 {
    140         self.0
    141     }
    142 
    143     /// Returns the sampled value as a duration.
    144     #[must_use]
    145     pub const fn duration(self) -> Duration {
    146         Duration::from_millis(self.0)
    147     }
    148 }
    149 
    150 /// Injected wall-time, monotonic-time, and entropy capabilities.
    151 pub struct RhiTimeEntropyAdapters {
    152     wall: Arc<dyn WallClock>,
    153     monotonic: Arc<dyn MonotonicClock>,
    154     entropy: Arc<dyn EntropySource>,
    155 }
    156 
    157 impl Clone for RhiTimeEntropyAdapters {
    158     fn clone(&self) -> Self {
    159         Self {
    160             wall: Arc::clone(&self.wall),
    161             monotonic: Arc::clone(&self.monotonic),
    162             entropy: Arc::clone(&self.entropy),
    163         }
    164     }
    165 }
    166 
    167 impl RhiTimeEntropyAdapters {
    168     /// Owns injected adapters without reading a clock or entropy source.
    169     pub fn new<W, M, E>(wall: W, monotonic: M, entropy: E) -> Self
    170     where
    171         W: WallClock + 'static,
    172         M: MonotonicClock + 'static,
    173         E: EntropySource + 'static,
    174     {
    175         Self {
    176             wall: Arc::new(wall),
    177             monotonic: Arc::new(monotonic),
    178             entropy: Arc::new(entropy),
    179         }
    180     }
    181 
    182     /// Constructs the production adapters without reading any value yet.
    183     #[must_use]
    184     pub fn system() -> Self {
    185         Self::new(SystemWallClock, SystemMonotonicClock::new(), SystemEntropy)
    186     }
    187 
    188     /// Reads one explicit whole-second UTC observation.
    189     pub fn now_utc(&self) -> Result<UnixTimeSeconds, RhiRuntimeAdapterError> {
    190         self.wall.now_utc().map_err(|_| {
    191             RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::WallClockUnavailable)
    192         })
    193     }
    194 
    195     pub(crate) fn now_utc_milliseconds(&self) -> Result<u64, RhiRuntimeAdapterError> {
    196         self.now_utc()?
    197             .get()
    198             .checked_mul(1_000)
    199             .filter(|value| *value <= i64::MAX as u64)
    200             .ok_or_else(|| {
    201                 RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::WallClockUnavailable)
    202             })
    203     }
    204 
    205     #[cfg(any(target_os = "linux", target_os = "macos"))]
    206     pub(crate) fn entropy(&self) -> &dyn EntropySource {
    207         self.entropy.as_ref()
    208     }
    209 
    210     /// Reads one observation from the injected process-local monotonic domain.
    211     #[must_use]
    212     pub fn now_monotonic(&self) -> MonotonicTime {
    213         self.monotonic.now_monotonic()
    214     }
    215 
    216     /// Computes a deadline in the injected monotonic domain without wrapping.
    217     pub fn deadline_after(
    218         &self,
    219         duration: Duration,
    220     ) -> Result<MonotonicDeadline, RhiRuntimeAdapterError> {
    221         self.monotonic.deadline_after(duration).map_err(|_| {
    222             RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid)
    223         })
    224     }
    225 
    226     /// Samples unbiased full jitter in the inclusive range `0..=maximum`.
    227     ///
    228     /// Rejection sampling is capped so an adversarial injected entropy source
    229     /// cannot keep one scheduler decision pending indefinitely.
    230     pub fn sample_full_jitter(
    231         &self,
    232         maximum: RhiJitterBoundMilliseconds,
    233     ) -> Result<RhiJitterMilliseconds, RhiRuntimeAdapterError> {
    234         let range = maximum.get() + 1;
    235         let rejection_threshold = range.wrapping_neg() % range;
    236         for _ in 0..RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS {
    237             let mut bytes = [0_u8; 8];
    238             self.entropy.fill_bytes(&mut bytes).map_err(|_| {
    239                 RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::EntropyUnavailable)
    240             })?;
    241             let product = u128::from(u64::from_be_bytes(bytes)) * u128::from(range);
    242             let low = u64::try_from(product & u128::from(u64::MAX))
    243                 .expect("masked multiply-high remainder fits u64");
    244             if low >= rejection_threshold {
    245                 let sampled = u64::try_from(product >> u64::BITS)
    246                     .expect("multiply-high full-jitter result fits the admitted u64 bound");
    247                 return Ok(RhiJitterMilliseconds(sampled));
    248             }
    249         }
    250         Err(RhiRuntimeAdapterError::new(
    251             RhiRuntimeAdapterErrorKind::EntropyUnavailable,
    252         ))
    253     }
    254 }
    255 
    256 impl fmt::Debug for RhiTimeEntropyAdapters {
    257     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    258         formatter.write_str("RhiTimeEntropyAdapters([injected])")
    259     }
    260 }
    261 
    262 /// Transport-neutral capabilities for bounded evidence fetch, live subscription, and publication.
    263 ///
    264 /// Construction performs no network, DNS, or TLS operation. The capabilities
    265 /// remain sealed inside RHI so concrete transports and detachable I/O handles
    266 /// do not become public runtime authority.
    267 pub struct RhiTransportAdapters {
    268     evidence_source: Arc<dyn EventSource>,
    269     evidence_subscriber: Arc<dyn EventSubscriber>,
    270     publication_sink: Arc<dyn EventSink>,
    271 }
    272 
    273 impl Clone for RhiTransportAdapters {
    274     fn clone(&self) -> Self {
    275         Self {
    276             evidence_source: Arc::clone(&self.evidence_source),
    277             evidence_subscriber: Arc::clone(&self.evidence_subscriber),
    278             publication_sink: Arc::clone(&self.publication_sink),
    279         }
    280     }
    281 }
    282 
    283 impl RhiTransportAdapters {
    284     /// Binds the complete transport-neutral capability inventory without I/O.
    285     #[must_use]
    286     pub fn new(
    287         evidence_source: Arc<dyn EventSource>,
    288         evidence_subscriber: Arc<dyn EventSubscriber>,
    289         publication_sink: Arc<dyn EventSink>,
    290     ) -> Self {
    291         Self {
    292             evidence_source,
    293             evidence_subscriber,
    294             publication_sink,
    295         }
    296     }
    297 
    298     pub(crate) fn evidence_source(&self) -> &dyn EventSource {
    299         self.evidence_source.as_ref()
    300     }
    301 
    302     #[cfg(any(target_os = "linux", target_os = "macos"))]
    303     pub(crate) fn evidence_subscriber(&self) -> &dyn EventSubscriber {
    304         self.evidence_subscriber.as_ref()
    305     }
    306 }
    307 
    308 impl fmt::Debug for RhiTransportAdapters {
    309     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    310         formatter.write_str("RhiTransportAdapters([sealed])")
    311     }
    312 }
    313 
    314 /// Injected read-existing-only wrapping-credential access.
    315 pub trait RhiCredentialAccess: Send + Sync {
    316     /// Resolves the configured credential for the exact runtime and identity binding.
    317     fn resolve_existing(
    318         &self,
    319         runtime: &RhiRuntimeContext,
    320         binding: &RhiIdentityEnvelopeBinding,
    321     ) -> Result<RhiWrappingCredential, RhiCredentialResolutionError>;
    322 }
    323 
    324 /// Injected read-existing-only encrypted-identity access.
    325 pub trait RhiIdentityAccess: Send + Sync {
    326     /// Opens and independently verifies the exact configured encrypted identity.
    327     fn open_existing(
    328         &self,
    329         binding: &RhiIdentityEnvelopeBinding,
    330         credential: &RhiWrappingCredential,
    331     ) -> Result<RhiDecryptedIdentity, RhiEncryptedIdentityEnvelopeError>;
    332 }
    333 
    334 /// Canonical credential resolver backed by the governed instance artifact boundary.
    335 #[derive(Clone, Copy, Debug, Default)]
    336 pub struct CanonicalRhiCredentialAccess;
    337 
    338 impl RhiCredentialAccess for CanonicalRhiCredentialAccess {
    339     fn resolve_existing(
    340         &self,
    341         runtime: &RhiRuntimeContext,
    342         binding: &RhiIdentityEnvelopeBinding,
    343     ) -> Result<RhiWrappingCredential, RhiCredentialResolutionError> {
    344         resolve_rhi_wrapping_credential(runtime, binding)
    345     }
    346 }
    347 
    348 /// Canonical encrypted-identity opener backed by the governed envelope boundary.
    349 #[derive(Clone, Copy, Debug, Default)]
    350 pub struct CanonicalRhiIdentityAccess;
    351 
    352 impl RhiIdentityAccess for CanonicalRhiIdentityAccess {
    353     fn open_existing(
    354         &self,
    355         binding: &RhiIdentityEnvelopeBinding,
    356         credential: &RhiWrappingCredential,
    357     ) -> Result<RhiDecryptedIdentity, RhiEncryptedIdentityEnvelopeError> {
    358         open_rhi_encrypted_identity(binding, credential)
    359     }
    360 }
    361 
    362 /// Ordered credential-then-identity access with no fallback or ambient selector.
    363 pub struct RhiIdentityCredentialAdapters {
    364     credential: Arc<dyn RhiCredentialAccess>,
    365     identity: Arc<dyn RhiIdentityAccess>,
    366 }
    367 
    368 impl RhiIdentityCredentialAdapters {
    369     /// Owns injected accessors without reading a credential or identity.
    370     #[must_use]
    371     pub fn new(
    372         credential: Arc<dyn RhiCredentialAccess>,
    373         identity: Arc<dyn RhiIdentityAccess>,
    374     ) -> Self {
    375         Self {
    376             credential,
    377             identity,
    378         }
    379     }
    380 
    381     /// Constructs the canonical read-existing-only accessors without performing I/O.
    382     #[must_use]
    383     pub fn canonical() -> Self {
    384         Self::new(
    385             Arc::new(CanonicalRhiCredentialAccess),
    386             Arc::new(CanonicalRhiIdentityAccess),
    387         )
    388     }
    389 
    390     /// Resolves the credential first, then opens and verifies the identity.
    391     pub fn open_existing(
    392         &self,
    393         runtime: &RhiRuntimeContext,
    394         binding: &RhiIdentityEnvelopeBinding,
    395     ) -> Result<RhiDecryptedIdentity, RhiRuntimeAdapterError> {
    396         let credential = self
    397             .credential
    398             .resolve_existing(runtime, binding)
    399             .map_err(|_| {
    400                 RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::CredentialAccess)
    401             })?;
    402         self.identity
    403             .open_existing(binding, &credential)
    404             .map_err(|_| RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::IdentityAccess))
    405     }
    406 }
    407 
    408 impl fmt::Debug for RhiIdentityCredentialAdapters {
    409     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    410         formatter.write_str("RhiIdentityCredentialAdapters([sealed])")
    411     }
    412 }
    413 
    414 /// Complete injected RHI foundation adapters with privately join-owned tasks.
    415 ///
    416 /// This value creates no runtime, installs no signal or logger, performs no
    417 /// transport or identity I/O, and exposes no task handle or supervisor.
    418 #[must_use = "runtime adapters retain join-owned task authority"]
    419 pub struct RhiRuntimeAdapters {
    420     time_entropy: RhiTimeEntropyAdapters,
    421     transport: RhiTransportAdapters,
    422     identity_credential: RhiIdentityCredentialAdapters,
    423     supervisor: TaskSupervisor,
    424 }
    425 
    426 impl RhiRuntimeAdapters {
    427     /// Composes already-constructed injected capabilities without invoking them.
    428     pub fn new(
    429         time_entropy: RhiTimeEntropyAdapters,
    430         transport: RhiTransportAdapters,
    431         identity_credential: RhiIdentityCredentialAdapters,
    432     ) -> Self {
    433         Self {
    434             time_entropy,
    435             transport,
    436             identity_credential,
    437             supervisor: TaskSupervisor::new(),
    438         }
    439     }
    440 
    441     /// Returns the injected time and entropy boundary.
    442     #[must_use]
    443     pub const fn time_entropy(&self) -> &RhiTimeEntropyAdapters {
    444         &self.time_entropy
    445     }
    446 
    447     /// Returns the ordered identity and credential boundary.
    448     #[must_use]
    449     pub const fn identity_credential(&self) -> &RhiIdentityCredentialAdapters {
    450         &self.identity_credential
    451     }
    452 
    453     #[cfg(any(target_os = "linux", target_os = "macos"))]
    454     pub(crate) const fn transport(&self) -> &RhiTransportAdapters {
    455         &self.transport
    456     }
    457 
    458     /// Returns the number of join-owned tasks currently registered.
    459     #[must_use]
    460     pub fn supervised_task_count(&self) -> usize {
    461         self.supervisor.task_count()
    462     }
    463 
    464     pub(crate) async fn shutdown(&mut self) -> Result<(), ()> {
    465         self.supervisor.request_cancellation();
    466         self.supervisor
    467             .supervise()
    468             .await
    469             .map(|_| ())
    470             .map_err(|_| ())
    471     }
    472 
    473     #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    474     pub(crate) fn supervisor_mut(&mut self) -> &mut TaskSupervisor {
    475         &mut self.supervisor
    476     }
    477 }
    478 
    479 impl fmt::Debug for RhiRuntimeAdapters {
    480     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    481         let _ = &self.transport;
    482         formatter
    483             .debug_struct("RhiRuntimeAdapters")
    484             .field("time_entropy", &"[injected]")
    485             .field("transport", &"[sealed]")
    486             .field("identity_credential", &"[sealed]")
    487             .field("supervised_task_count", &self.supervised_task_count())
    488             .finish()
    489     }
    490 }
    491 
    492 #[cfg(test)]
    493 mod tests {
    494     use core::{
    495         future::ready,
    496         sync::atomic::{AtomicUsize, Ordering},
    497     };
    498 
    499     use radroots_service_host::{
    500         EntropyError, HostError, MonotonicClockError, ShutdownPhase, TaskClassification,
    501         TaskMetadata, TaskName, WallClockError,
    502     };
    503     use radroots_transport::{
    504         BoxFuture, DeliveryReceipt, DeliveryRequest, EventSubscription, FetchPage, FetchRequest,
    505         SinkFailure, SinkStatus, SourceStatus, SubscriptionRequest,
    506     };
    507 
    508     use super::*;
    509 
    510     #[derive(Clone, Copy)]
    511     struct FixedWall(Result<UnixTimeSeconds, WallClockError>);
    512 
    513     impl WallClock for FixedWall {
    514         fn now_utc(&self) -> Result<UnixTimeSeconds, WallClockError> {
    515             self.0
    516         }
    517     }
    518 
    519     #[derive(Clone, Copy)]
    520     struct FixedMonotonic(MonotonicTime);
    521 
    522     impl MonotonicClock for FixedMonotonic {
    523         fn now_monotonic(&self) -> MonotonicTime {
    524             self.0
    525         }
    526     }
    527 
    528     #[derive(Clone, Copy)]
    529     struct FixedEntropy(Result<u64, EntropyError>);
    530 
    531     impl EntropySource for FixedEntropy {
    532         fn fill_bytes(&self, destination: &mut [u8]) -> Result<(), EntropyError> {
    533             let value = self.0?;
    534             destination.copy_from_slice(&value.to_be_bytes());
    535             Ok(())
    536         }
    537     }
    538 
    539     struct NoIoTransport;
    540 
    541     impl EventSource for NoIoTransport {
    542         fn status(&self) -> BoxFuture<'_, Result<SourceStatus, radroots_transport::Error>> {
    543             Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
    544         }
    545 
    546         fn fetch(
    547             &self,
    548             _request: FetchRequest,
    549         ) -> BoxFuture<'_, Result<FetchPage, radroots_transport::Error>> {
    550             Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
    551         }
    552     }
    553 
    554     impl EventSubscriber for NoIoTransport {
    555         fn subscribe(
    556             &self,
    557             _request: SubscriptionRequest,
    558         ) -> BoxFuture<'_, Result<Box<dyn EventSubscription>, radroots_transport::Error>> {
    559             Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
    560         }
    561     }
    562 
    563     impl EventSink for NoIoTransport {
    564         fn status(&self) -> BoxFuture<'_, Result<SinkStatus, radroots_transport::Error>> {
    565             Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
    566         }
    567 
    568         fn deliver(
    569             &self,
    570             request: DeliveryRequest,
    571         ) -> BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> {
    572             Box::pin(ready(Err(SinkFailure::invalid_contract(&request))))
    573         }
    574     }
    575 
    576     #[test]
    577     fn runtime_adapter_contract_is_exact_and_defers_process_authority() {
    578         let contract: serde_json::Value =
    579             serde_json::from_str(RUNTIME_ADAPTER_CONTRACT).expect("runtime adapter contract");
    580         assert_eq!(
    581             contract,
    582             serde_json::json!({
    583                 "schema": "radroots.rhi.runtime-adapters",
    584                 "schema_version": 1,
    585                 "contract_version": RHI_RUNTIME_ADAPTER_CONTRACT_VERSION,
    586                 "time_entropy": {
    587                     "wall_time": "injected_whole_second_utc",
    588                     "monotonic_time": "injected_process_local_domain",
    589                     "entropy": "injected_complete_fill_or_error",
    590                     "event_authored_time": "untrusted_input"
    591                 },
    592                 "jitter": {
    593                     "algorithm": "rejection_sampled_multiply_high_full_jitter",
    594                     "unit": "milliseconds",
    595                     "inclusive_minimum": 0,
    596                     "inclusive_maximum": RHI_RUNTIME_JITTER_MAX_MILLISECONDS,
    597                     "maximum_entropy_draws": RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS,
    598                     "wall_clock_derived": false
    599                 },
    600                 "transport": {
    601                     "contract": "radroots_transport",
    602                     "evidence_fetch": "EventSource",
    603                     "evidence_subscription": "EventSubscriber",
    604                     "publication": "EventSink",
    605                     "construction_performs_io": false,
    606                     "concrete_handles_exposed": false
    607                 },
    608                 "identity": {
    609                     "order": ["credential", "encrypted_identity"],
    610                     "credential": "read_existing_canonical_instance_artifact",
    611                     "encrypted_identity": "read_existing_and_independently_verify",
    612                     "fallback": false,
    613                     "generation": false
    614                 },
    615                 "tasks": {
    616                     "supervisor": "radroots_service_host::TaskSupervisor",
    617                     "join_owned": true,
    618                     "handles_exposed": false
    619                 },
    620                 "library_exclusions": [
    621                     "signal_installation",
    622                     "runtime_creation",
    623                     "logging_installation",
    624                     "process_exit",
    625                     "detached_tasks"
    626                 ]
    627             })
    628         );
    629     }
    630 
    631     #[test]
    632     fn injected_time_deadline_and_full_jitter_are_exactly_bounded() {
    633         let now = MonotonicTime::from_duration_since_origin(Duration::from_millis(40));
    634         let minimum = RhiTimeEntropyAdapters::new(
    635             FixedWall(Ok(UnixTimeSeconds::new(1_000))),
    636             FixedMonotonic(now),
    637             FixedEntropy(Ok(1)),
    638         );
    639         assert_eq!(minimum.now_utc().expect("wall").get(), 1_000);
    640         assert_eq!(minimum.now_monotonic(), now);
    641         assert_eq!(
    642             minimum
    643                 .deadline_after(Duration::from_millis(2))
    644                 .expect("deadline")
    645                 .time()
    646                 .duration_since_origin(),
    647             Duration::from_millis(42)
    648         );
    649         let maximum = RhiJitterBoundMilliseconds::new(RHI_RUNTIME_JITTER_MAX_MILLISECONDS)
    650             .expect("maximum bound");
    651         assert_eq!(
    652             minimum.sample_full_jitter(maximum).expect("minimum").get(),
    653             0
    654         );
    655 
    656         let upper = RhiTimeEntropyAdapters::new(
    657             FixedWall(Ok(UnixTimeSeconds::new(1))),
    658             FixedMonotonic(now),
    659             FixedEntropy(Ok(u64::MAX)),
    660         );
    661         assert_eq!(
    662             upper.sample_full_jitter(maximum).expect("maximum").get(),
    663             maximum.get()
    664         );
    665         assert_eq!(
    666             upper
    667                 .sample_full_jitter(RhiJitterBoundMilliseconds::new(0).expect("zero"))
    668                 .expect("zero sample")
    669                 .duration(),
    670             Duration::ZERO
    671         );
    672         assert_eq!(
    673             RhiJitterBoundMilliseconds::new(RHI_RUNTIME_JITTER_MAX_MILLISECONDS + 1)
    674                 .expect_err("above maximum")
    675                 .kind(),
    676             RhiRuntimeAdapterErrorKind::InvalidJitterBound
    677         );
    678 
    679         let rejected = RhiTimeEntropyAdapters::new(
    680             FixedWall(Ok(UnixTimeSeconds::new(1))),
    681             FixedMonotonic(now),
    682             FixedEntropy(Ok(0)),
    683         );
    684         assert_eq!(
    685             rejected
    686                 .sample_full_jitter(RhiJitterBoundMilliseconds::new(2).expect("bound"))
    687                 .expect_err("bounded rejection")
    688                 .kind(),
    689             RhiRuntimeAdapterErrorKind::EntropyUnavailable
    690         );
    691     }
    692 
    693     #[test]
    694     fn injected_failures_and_deadline_overflow_are_stable_and_source_free() {
    695         let maximum_time = MonotonicTime::from_duration_since_origin(Duration::MAX);
    696         let adapters = RhiTimeEntropyAdapters::new(
    697             FixedWall(Err(WallClockError::BeforeUnixEpoch)),
    698             FixedMonotonic(maximum_time),
    699             FixedEntropy(Err(EntropyError::Unavailable)),
    700         );
    701         let cases = [
    702             (
    703                 adapters.now_utc().expect_err("wall").kind(),
    704                 RhiRuntimeAdapterErrorKind::WallClockUnavailable,
    705             ),
    706             (
    707                 adapters
    708                     .deadline_after(Duration::from_millis(1))
    709                     .expect_err("deadline")
    710                     .kind(),
    711                 RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid,
    712             ),
    713             (
    714                 adapters
    715                     .sample_full_jitter(RhiJitterBoundMilliseconds::new(1).expect("bound"))
    716                     .expect_err("entropy")
    717                     .kind(),
    718                 RhiRuntimeAdapterErrorKind::EntropyUnavailable,
    719             ),
    720         ];
    721         for (actual, expected) in cases {
    722             assert_eq!(actual, expected);
    723             let error = RhiRuntimeAdapterError::new(actual);
    724             assert!(Error::source(&error).is_none());
    725             assert!(!format!("{error:?} {error}").contains("secret"));
    726         }
    727         assert_eq!(
    728             maximum_time.checked_deadline_after(Duration::from_millis(1)),
    729             Err(MonotonicClockError::DeadlineOverflow)
    730         );
    731     }
    732 
    733     #[tokio::test]
    734     async fn adapter_set_is_inert_until_invoked_and_owns_joined_tasks() {
    735         let transport = Arc::new(NoIoTransport);
    736         let transports = RhiTransportAdapters::new(transport.clone(), transport.clone(), transport);
    737         let mut adapters = RhiRuntimeAdapters::new(
    738             RhiTimeEntropyAdapters::new(
    739                 FixedWall(Ok(UnixTimeSeconds::new(1))),
    740                 FixedMonotonic(MonotonicTime::from_duration_since_origin(Duration::ZERO)),
    741                 FixedEntropy(Ok(1)),
    742             ),
    743             transports,
    744             RhiIdentityCredentialAdapters::canonical(),
    745         );
    746         assert_eq!(adapters.supervised_task_count(), 0);
    747         let calls = Arc::new(AtomicUsize::new(0));
    748         let task_calls = Arc::clone(&calls);
    749         adapters
    750             .supervisor_mut()
    751             .spawn(
    752                 TaskMetadata::new(
    753                     TaskName::new("adapter_contract_test").expect("task name"),
    754                     TaskClassification::OneShot,
    755                     None,
    756                 )
    757                 .expect("metadata"),
    758                 move |_cancel| async move {
    759                     task_calls.fetch_add(1, Ordering::Relaxed);
    760                     Ok::<(), HostError>(())
    761                 },
    762             )
    763             .expect("register");
    764         assert_eq!(adapters.supervised_task_count(), 1);
    765         assert_eq!(
    766             adapters
    767                 .supervisor_mut()
    768                 .supervise()
    769                 .await
    770                 .expect("joined")
    771                 .len(),
    772             1
    773         );
    774         assert_eq!(calls.load(Ordering::Relaxed), 1);
    775         assert_eq!(adapters.supervised_task_count(), 0);
    776         assert_eq!(
    777             format!("{adapters:?}"),
    778             "RhiRuntimeAdapters { time_entropy: \"[injected]\", transport: \"[sealed]\", identity_credential: \"[sealed]\", supervised_task_count: 0 }"
    779         );
    780     }
    781 
    782     #[tokio::test]
    783     async fn adapter_shutdown_cancels_and_joins_long_lived_tasks() {
    784         let transport = Arc::new(NoIoTransport);
    785         let transports = RhiTransportAdapters::new(transport.clone(), transport.clone(), transport);
    786         let mut adapters = RhiRuntimeAdapters::new(
    787             RhiTimeEntropyAdapters::new(
    788                 FixedWall(Ok(UnixTimeSeconds::new(1))),
    789                 FixedMonotonic(MonotonicTime::from_duration_since_origin(Duration::ZERO)),
    790                 FixedEntropy(Ok(1)),
    791             ),
    792             transports,
    793             RhiIdentityCredentialAdapters::canonical(),
    794         );
    795         let cancellations = Arc::new(AtomicUsize::new(0));
    796         let task_cancellations = Arc::clone(&cancellations);
    797         adapters
    798             .supervisor_mut()
    799             .spawn(
    800                 TaskMetadata::new(
    801                     TaskName::new("adapter_cancellation_test").expect("task name"),
    802                     TaskClassification::Critical,
    803                     Some(ShutdownPhase::CloseNetwork),
    804                 )
    805                 .expect("metadata"),
    806                 move |cancel| async move {
    807                     cancel.cancelled().await;
    808                     task_cancellations.fetch_add(1, Ordering::Relaxed);
    809                     Ok::<(), HostError>(())
    810                 },
    811             )
    812             .expect("register");
    813         adapters.shutdown().await.expect("joined shutdown");
    814         assert_eq!(cancellations.load(Ordering::Relaxed), 1);
    815         assert_eq!(adapters.supervised_task_count(), 0);
    816     }
    817 
    818     #[test]
    819     fn all_error_codes_messages_and_debug_are_stable() {
    820         let cases = [
    821             (
    822                 RhiRuntimeAdapterErrorKind::InvalidJitterBound,
    823                 "runtime_jitter_bound_invalid",
    824                 "RHI jitter bound is invalid",
    825             ),
    826             (
    827                 RhiRuntimeAdapterErrorKind::EntropyUnavailable,
    828                 "runtime_entropy_unavailable",
    829                 "RHI entropy source is unavailable",
    830             ),
    831             (
    832                 RhiRuntimeAdapterErrorKind::WallClockUnavailable,
    833                 "runtime_wall_clock_unavailable",
    834                 "RHI wall clock is unavailable",
    835             ),
    836             (
    837                 RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid,
    838                 "runtime_monotonic_deadline_invalid",
    839                 "RHI monotonic deadline is invalid",
    840             ),
    841             (
    842                 RhiRuntimeAdapterErrorKind::CredentialAccess,
    843                 "runtime_credential_access_failed",
    844                 "RHI credential access failed",
    845             ),
    846             (
    847                 RhiRuntimeAdapterErrorKind::IdentityAccess,
    848                 "runtime_identity_access_failed",
    849                 "RHI identity access failed",
    850             ),
    851         ];
    852         for (kind, code, message) in cases {
    853             let error = RhiRuntimeAdapterError::new(kind);
    854             assert_eq!(error.code(), code);
    855             assert_eq!(error.to_string(), message);
    856             assert_eq!(
    857                 format!("{error:?}"),
    858                 format!("RhiRuntimeAdapterError {{ kind: {kind:?} }}")
    859             );
    860             assert!(Error::source(&error).is_none());
    861         }
    862     }
    863 }