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 }