commit 7978b532627281c711a0d599106078d20f1b54c5
parent ef53ce07d5eaac77f388b48d9ec73e7a0922de8b
Author: triesap <tyson@radroots.org>
Date: Sun, 23 Aug 2026 22:49:09 +0000
runtime(rhi): define injected adapter boundary
Diffstat:
11 files changed, 1064 insertions(+), 4 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -213,6 +213,17 @@
evidence sources, and failpoints. Supervise and join every authoritative task;
panic, error, or unexpected successful return from a critical task must
coordinate shutdown and produce a nonzero process result.
+- Compose those dependencies only through the sealed runtime-adapter boundary.
+ Wall UTC and process-local monotonic observations remain distinct; jitter is
+ bounded whole-millisecond full jitter derived only from injected entropy.
+ The transport-neutral `radroots_transport` source, subscription, and sink
+ traits are the sole generic event I/O SPI. Credential access must precede
+ independently verified encrypted-identity access, with no fallback or
+ generation. The adapter set owns one private shared `TaskSupervisor` and
+ exposes no task handle or concrete transport handle.
+- Library code must not install signals, create a runtime, install logging,
+ call process exit, or detach an authoritative task. Those process authorities
+ remain exclusively with the final binary checkpoint.
- On startup, reclaim expired reconciliation/publication leases, resume durable
retry schedules with injected bounded jitter, retain unknown submissions,
finalize already-proven outcomes idempotently, and scan all authoritative
diff --git a/Cargo.lock b/Cargo.lock
@@ -1838,6 +1838,7 @@ dependencies = [
"radroots_service_sqlite",
"radroots_storage",
"radroots_trade",
+ "radroots_transport",
"rustix",
"serde",
"serde_json",
diff --git a/Cargo.toml b/Cargo.toml
@@ -51,6 +51,7 @@ radroots_runtime_paths = { git = "https://github.com/radrootslabs/lib", rev = "7
radroots_service_host = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
radroots_service_sqlite = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
radroots_storage = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha", default-features = false }
+radroots_transport = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha", default-features = false, features = ["std"] }
radroots_secrets = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
radroots_trade = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
diff --git a/README b/README
@@ -12,6 +12,11 @@ runtime-path, service-SQLite, storage, event, and trade values that appear in
signatures are deliberate governed contract types; raw SQLx, Serde, transport,
filesystem, and task authority never crosses this boundary.
+The shared clock and entropy traits and their source-free error values are also
+deliberate governed injection contracts. RHI operations normalize their
+failures into stable RHI classifications; concrete system adapters remain
+private implementation details.
+
Child modules cannot bypass the reviewed root surface:
```compile_fail
@@ -28,6 +33,30 @@ RHI publishes its service kind-0 Profile only through the sealed
`RadrootsAuthoredProfile` replacement snapshot. It does not retain a generic
kind-0 event-authoring path.
+## Injected runtime adapters
+
+`RhiRuntimeAdapters` is the sealed composition boundary for all runtime inputs
+that must remain replaceable in deterministic tests. It owns distinct injected
+whole-second wall UTC, process-local monotonic time, and entropy sources; a
+transport-neutral bounded event source, live subscriber, and publication sink;
+ordered read-existing credential and independently verified encrypted-identity
+access; and one private shared join-owning task supervisor.
+
+Full jitter is measured only in whole milliseconds, is sampled from injected
+entropy with rejection-sampled multiply-high mapping, is always in the
+inclusive configured range, and is capped at the exact v1 maximum of 3,600,000
+milliseconds. Sampling fails closed after sixteen rejected entropy draws, so a
+bad adapter cannot hang scheduling. Jitter is never derived from wall-clock
+nanoseconds. Event-authored time remains untrusted input and cannot substitute
+for an injected observation time or a monotonic deadline.
+
+Constructing the adapter set performs no clock read, entropy read, identity or
+credential access, DNS lookup, network operation, or task spawn. Concrete
+transport handles and the task supervisor remain sealed. The library installs
+no signal handler, Tokio runtime, logger, or process-exit policy; the final
+binary checkpoint owns those authorities. The exact machine contract is
+[`runtime_adapters.v1.json`](contracts/services_hardening/runtime_adapters.v1.json).
+
## Hardened v1 configuration contract
The target service configuration is frozen by
diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt
@@ -1,6 +1,12 @@
pub mod rhi
+pub use rhi::EntropyError
+pub use rhi::EntropySource
pub use rhi::INSTANCE_ID_MAX_BYTES
pub use rhi::InstanceId
+pub use rhi::MonotonicClock
+pub use rhi::MonotonicClockError
+pub use rhi::MonotonicDeadline
+pub use rhi::MonotonicTime
pub use rhi::RadrootsHostEnvironment
pub use rhi::RadrootsPathProfile
pub use rhi::RadrootsPathResolver
@@ -9,6 +15,9 @@ pub use rhi::RadrootsServiceInstanceArtifacts
pub use rhi::RuntimeContext
pub use rhi::RuntimeContextSource
pub use rhi::ServiceId
+pub use rhi::UnixTimeSeconds
+pub use rhi::WallClock
+pub use rhi::WallClockError
pub enum rhi::RhiBootstrapProfileV1
pub rhi::RhiBootstrapProfileV1::Interactive
pub rhi::RhiBootstrapProfileV1::RepoLocal
@@ -122,6 +131,15 @@ pub enum rhi::RhiReconciliationCommandV1
pub rhi::RhiReconciliationCommandV1::Jobs
pub rhi::RhiReconciliationCommandV1::Refresh
pub rhi::RhiReconciliationCommandV1::Status
+pub enum rhi::RhiRuntimeAdapterErrorKind
+pub rhi::RhiRuntimeAdapterErrorKind::CredentialAccess
+pub rhi::RhiRuntimeAdapterErrorKind::EntropyUnavailable
+pub rhi::RhiRuntimeAdapterErrorKind::IdentityAccess
+pub rhi::RhiRuntimeAdapterErrorKind::InvalidJitterBound
+pub rhi::RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid
+pub rhi::RhiRuntimeAdapterErrorKind::WallClockUnavailable
+impl rhi::RhiRuntimeAdapterErrorKind
+pub const fn rhi::RhiRuntimeAdapterErrorKind::code(self) -> &'static str
pub enum rhi::RhiRuntimeContextErrorKind
pub rhi::RhiRuntimeContextErrorKind::InvalidBootstrapBinding
pub rhi::RhiRuntimeContextErrorKind::InvalidServiceIdentity
@@ -224,6 +242,12 @@ impl rhi::TradeAgreementAttestationErrorKind
pub const fn rhi::TradeAgreementAttestationErrorKind::code(self) -> &'static str
impl core::fmt::Display for rhi::TradeAgreementAttestationErrorKind
pub fn rhi::TradeAgreementAttestationErrorKind::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::CanonicalRhiCredentialAccess
+impl rhi::RhiCredentialAccess for rhi::CanonicalRhiCredentialAccess
+pub fn rhi::CanonicalRhiCredentialAccess::resolve_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError>
+pub struct rhi::CanonicalRhiIdentityAccess
+impl rhi::RhiIdentityAccess for rhi::CanonicalRhiIdentityAccess
+pub fn rhi::CanonicalRhiIdentityAccess::open_existing(&self, &rhi::RhiIdentityEnvelopeBinding, &rhi::RhiWrappingCredential) -> core::result::Result<rhi::RhiDecryptedIdentity, rhi::RhiEncryptedIdentityEnvelopeError>
pub struct rhi::NostrEventAdapter<'a>
impl<'a> rhi::NostrEventAdapter<'a>
pub fn rhi::NostrEventAdapter<'a>::new(&'a nostr::event::Event) -> Self
@@ -341,6 +365,13 @@ impl rhi::RhiExpectedPublicIdentity
pub fn rhi::RhiExpectedPublicIdentity::as_hex(&self) -> &str
impl core::fmt::Debug for rhi::RhiExpectedPublicIdentity
pub fn rhi::RhiExpectedPublicIdentity::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiIdentityCredentialAdapters
+impl rhi::RhiIdentityCredentialAdapters
+pub fn rhi::RhiIdentityCredentialAdapters::canonical() -> Self
+pub fn rhi::RhiIdentityCredentialAdapters::new(alloc::sync::Arc<dyn rhi::RhiCredentialAccess>, alloc::sync::Arc<dyn rhi::RhiIdentityAccess>) -> Self
+pub fn rhi::RhiIdentityCredentialAdapters::open_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiDecryptedIdentity, rhi::RhiRuntimeAdapterError>
+impl core::fmt::Debug for rhi::RhiIdentityCredentialAdapters
+pub fn rhi::RhiIdentityCredentialAdapters::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiIdentityEnvelopeBinding
impl rhi::RhiIdentityEnvelopeBinding
pub const fn rhi::RhiIdentityEnvelopeBinding::expected_identity(&self) -> &rhi::RhiExpectedPublicIdentity
@@ -349,6 +380,14 @@ pub const fn rhi::RhiIdentityEnvelopeBinding::kind(&self) -> rhi::RhiIdentityPro
pub const fn rhi::RhiIdentityEnvelopeBinding::role(&self) -> rhi::RhiIdentityRole
impl core::fmt::Debug for rhi::RhiIdentityEnvelopeBinding
pub fn rhi::RhiIdentityEnvelopeBinding::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiJitterBoundMilliseconds(_)
+impl rhi::RhiJitterBoundMilliseconds
+pub const fn rhi::RhiJitterBoundMilliseconds::get(self) -> u64
+pub const fn rhi::RhiJitterBoundMilliseconds::new(u64) -> core::result::Result<Self, rhi::RhiRuntimeAdapterError>
+pub struct rhi::RhiJitterMilliseconds(_)
+impl rhi::RhiJitterMilliseconds
+pub const fn rhi::RhiJitterMilliseconds::duration(self) -> core::time::Duration
+pub const fn rhi::RhiJitterMilliseconds::get(self) -> u64
pub struct rhi::RhiMutationRepository<'host>
impl rhi::RhiMutationRepository<'_>
pub const fn rhi::RhiMutationRepository<'_>::descriptor(&self) -> rhi::RhiStateRepositoryDescriptor
@@ -408,6 +447,23 @@ pub const fn rhi::RhiReportRepository<'_>::descriptor(&self) -> rhi::RhiStateRep
pub const fn rhi::RhiReportRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiReportRepository<'_>
pub fn rhi::RhiReportRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiRuntimeAdapterError
+impl rhi::RhiRuntimeAdapterError
+pub const fn rhi::RhiRuntimeAdapterError::code(self) -> &'static str
+pub const fn rhi::RhiRuntimeAdapterError::kind(self) -> rhi::RhiRuntimeAdapterErrorKind
+impl core::error::Error for rhi::RhiRuntimeAdapterError
+impl core::fmt::Debug for rhi::RhiRuntimeAdapterError
+pub fn rhi::RhiRuntimeAdapterError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for rhi::RhiRuntimeAdapterError
+pub fn rhi::RhiRuntimeAdapterError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiRuntimeAdapters
+impl rhi::RhiRuntimeAdapters
+pub const fn rhi::RhiRuntimeAdapters::identity_credential(&self) -> &rhi::RhiIdentityCredentialAdapters
+pub fn rhi::RhiRuntimeAdapters::new(rhi::RhiTimeEntropyAdapters, rhi::RhiTransportAdapters, rhi::RhiIdentityCredentialAdapters) -> Self
+pub fn rhi::RhiRuntimeAdapters::supervised_task_count(&self) -> usize
+pub const fn rhi::RhiRuntimeAdapters::time_entropy(&self) -> &rhi::RhiTimeEntropyAdapters
+impl core::fmt::Debug for rhi::RhiRuntimeAdapters
+pub fn rhi::RhiRuntimeAdapters::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiRuntimeContext
impl rhi::RhiRuntimeContext
pub const fn rhi::RhiRuntimeContext::artifacts(&self) -> &radroots_runtime_paths::conventions::RadrootsServiceInstanceArtifacts
@@ -561,6 +617,21 @@ pub const fn rhi::RhiSupersessionRepository<'_>::descriptor(&self) -> rhi::RhiSt
pub const fn rhi::RhiSupersessionRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind
impl core::fmt::Debug for rhi::RhiSupersessionRepository<'_>
pub fn rhi::RhiSupersessionRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiTimeEntropyAdapters
+impl rhi::RhiTimeEntropyAdapters
+pub fn rhi::RhiTimeEntropyAdapters::deadline_after(&self, core::time::Duration) -> core::result::Result<radroots_service_host::time::MonotonicDeadline, rhi::RhiRuntimeAdapterError>
+pub fn rhi::RhiTimeEntropyAdapters::new<W, M, E>(W, M, E) -> Self where W: radroots_service_host::time::WallClock + 'static, M: radroots_service_host::time::MonotonicClock + 'static, E: radroots_service_host::entropy::EntropySource + 'static
+pub fn rhi::RhiTimeEntropyAdapters::now_monotonic(&self) -> radroots_service_host::time::MonotonicTime
+pub fn rhi::RhiTimeEntropyAdapters::now_utc(&self) -> core::result::Result<radroots_service_host::time::UnixTimeSeconds, rhi::RhiRuntimeAdapterError>
+pub fn rhi::RhiTimeEntropyAdapters::sample_full_jitter(&self, rhi::RhiJitterBoundMilliseconds) -> core::result::Result<rhi::RhiJitterMilliseconds, rhi::RhiRuntimeAdapterError>
+pub fn rhi::RhiTimeEntropyAdapters::system() -> Self
+impl core::fmt::Debug for rhi::RhiTimeEntropyAdapters
+pub fn rhi::RhiTimeEntropyAdapters::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct rhi::RhiTransportAdapters
+impl rhi::RhiTransportAdapters
+pub fn rhi::RhiTransportAdapters::new(alloc::sync::Arc<dyn radroots_transport::source::EventSource>, alloc::sync::Arc<dyn radroots_transport::source::EventSubscriber>, alloc::sync::Arc<dyn radroots_transport::sink::EventSink>) -> Self
+impl core::fmt::Debug for rhi::RhiTransportAdapters
+pub fn rhi::RhiTransportAdapters::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct rhi::RhiVerifiedStateBackup
impl rhi::RhiVerifiedStateBackup
pub const fn rhi::RhiVerifiedStateBackup::database_metadata(&self) -> &radroots_service_sqlite::metadata::ServiceDatabaseMetadata
@@ -630,6 +701,9 @@ pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_CONTRACT_VERSION: u32
pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES: usize
pub const rhi::RHI_MIGRATION_CATALOG_SHA256: [u8; 32]
pub const rhi::RHI_PROVIDER_CONTRACT_VERSION: u32
+pub const rhi::RHI_RUNTIME_ADAPTER_CONTRACT_VERSION: u32
+pub const rhi::RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS: usize
+pub const rhi::RHI_RUNTIME_JITTER_MAX_MILLISECONDS: u64
pub const rhi::RHI_STATE_APPLICATION_ID: u32
pub const rhi::RHI_STATE_REPOSITORY_CONTRACT_VERSION: u32
pub const rhi::RHI_STATE_REPOSITORY_COUNT: usize
@@ -640,6 +714,14 @@ pub const rhi::RHI_STATE_SCHEMA_VERSION_1_SHA256: [u8; 32]
pub const rhi::RHI_STATUS_CONTRACT_VERSION: u32
pub const rhi::RHI_WRAPPING_CREDENTIAL_ARTIFACT_BYTES: usize
pub const rhi::RHI_WRAPPING_CREDENTIAL_CONTRACT_VERSION: u32
+pub trait rhi::RhiCredentialAccess: core::marker::Send + core::marker::Sync
+pub fn rhi::RhiCredentialAccess::resolve_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError>
+impl rhi::RhiCredentialAccess for rhi::CanonicalRhiCredentialAccess
+pub fn rhi::CanonicalRhiCredentialAccess::resolve_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError>
+pub trait rhi::RhiIdentityAccess: core::marker::Send + core::marker::Sync
+pub fn rhi::RhiIdentityAccess::open_existing(&self, &rhi::RhiIdentityEnvelopeBinding, &rhi::RhiWrappingCredential) -> core::result::Result<rhi::RhiDecryptedIdentity, rhi::RhiEncryptedIdentityEnvelopeError>
+impl rhi::RhiIdentityAccess for rhi::CanonicalRhiIdentityAccess
+pub fn rhi::CanonicalRhiIdentityAccess::open_existing(&self, &rhi::RhiIdentityEnvelopeBinding, &rhi::RhiWrappingCredential) -> core::result::Result<rhi::RhiDecryptedIdentity, rhi::RhiEncryptedIdentityEnvelopeError>
pub fn rhi::attest_projection_claim(&radroots_trade::trade_contract_v1::RadrootsTradeProjectionV1, &radroots_event::id::MutationId, &rhi::TradeAgreementAttestationPolicy) -> core::result::Result<rhi::TradeAgreementAttestationReportV1, rhi::TradeAgreementAttestationError>
pub async fn rhi::finalize_rhi_state_restore(rhi::RhiStagedStateRestore) -> core::result::Result<(), rhi::RhiStateMaintenanceError>
pub async fn rhi::initialize_rhi_state(&rhi::RhiRuntimeContext, &rhi::RhiStateMetadata, radroots_service_sqlite::migration::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<(), rhi::RhiStateHostError>
diff --git a/contracts/services_hardening/runtime_adapters.v1.json b/contracts/services_hardening/runtime_adapters.v1.json
@@ -0,0 +1,46 @@
+{
+ "schema": "radroots.rhi.runtime-adapters",
+ "schema_version": 1,
+ "contract_version": 1,
+ "time_entropy": {
+ "wall_time": "injected_whole_second_utc",
+ "monotonic_time": "injected_process_local_domain",
+ "entropy": "injected_complete_fill_or_error",
+ "event_authored_time": "untrusted_input"
+ },
+ "jitter": {
+ "algorithm": "rejection_sampled_multiply_high_full_jitter",
+ "unit": "milliseconds",
+ "inclusive_minimum": 0,
+ "inclusive_maximum": 3600000,
+ "maximum_entropy_draws": 16,
+ "wall_clock_derived": false
+ },
+ "transport": {
+ "contract": "radroots_transport",
+ "evidence_fetch": "EventSource",
+ "evidence_subscription": "EventSubscriber",
+ "publication": "EventSink",
+ "construction_performs_io": false,
+ "concrete_handles_exposed": false
+ },
+ "identity": {
+ "order": ["credential", "encrypted_identity"],
+ "credential": "read_existing_canonical_instance_artifact",
+ "encrypted_identity": "read_existing_and_independently_verify",
+ "fallback": false,
+ "generation": false
+ },
+ "tasks": {
+ "supervisor": "radroots_service_host::TaskSupervisor",
+ "join_owned": true,
+ "handles_exposed": false
+ },
+ "library_exclusions": [
+ "signal_installation",
+ "runtime_creation",
+ "logging_installation",
+ "process_exit",
+ "detached_tasks"
+ ]
+}
diff --git a/radroots.service.source-lock.v2.toml b/radroots.service.source-lock.v2.toml
@@ -7,7 +7,7 @@ architecture = "radroots.crates.release.v2"
workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4"
version = "0.1.0-alpha"
source_archive_sha256 = "b425371c134be96cce46b37f7035d6212f1efe8cff50bef366631ba5632991b0"
-cargo_lock_sha256 = "acf51e7848c64d0361b5d00f5035edc93daa6d3b3f73256ba3718c6b4f498db9"
+cargo_lock_sha256 = "f519a64d8093610f9536a0911ddac85412e2f6aef2724cb09b5af5b32ab9654b"
rust_version = "1.97.1"
host_feature_profile = "service-host"
diff --git a/src/lib.rs b/src/lib.rs
@@ -8,6 +8,7 @@ mod config_v1;
mod features;
mod identity_credential;
mod identity_envelope;
+mod runtime_adapters;
mod runtime_context;
mod state_catalog;
mod state_host;
@@ -55,6 +56,17 @@ pub use radroots_runtime_paths::{
RadrootsPathResolver, RadrootsPlatform, RadrootsServiceInstanceArtifacts, RuntimeContext,
RuntimeContextSource, ServiceId,
};
+pub use radroots_service_host::{
+ EntropyError, EntropySource, MonotonicClock, MonotonicClockError, MonotonicDeadline,
+ MonotonicTime, UnixTimeSeconds, WallClock, WallClockError,
+};
+pub use runtime_adapters::{
+ CanonicalRhiCredentialAccess, CanonicalRhiIdentityAccess, RHI_RUNTIME_ADAPTER_CONTRACT_VERSION,
+ RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS, RHI_RUNTIME_JITTER_MAX_MILLISECONDS, RhiCredentialAccess,
+ RhiIdentityAccess, RhiIdentityCredentialAdapters, RhiJitterBoundMilliseconds,
+ RhiJitterMilliseconds, RhiRuntimeAdapterError, RhiRuntimeAdapterErrorKind, RhiRuntimeAdapters,
+ RhiTimeEntropyAdapters, RhiTransportAdapters,
+};
pub use runtime_context::{
RhiRuntimeContext, RhiRuntimeContextError, RhiRuntimeContextErrorKind,
resolve_rhi_runtime_context,
diff --git a/src/runtime_adapters.rs b/src/runtime_adapters.rs
@@ -0,0 +1,768 @@
+//! Injected, bounded runtime capability composition.
+
+use core::{fmt, time::Duration};
+use std::{error::Error, sync::Arc};
+
+use radroots_service_host::{
+ EntropySource, MonotonicClock, MonotonicDeadline, MonotonicTime, SystemEntropy,
+ SystemMonotonicClock, SystemWallClock, TaskSupervisor, UnixTimeSeconds, WallClock,
+};
+use radroots_transport::{EventSink, EventSource, EventSubscriber};
+
+use crate::{
+ RhiCredentialResolutionError, RhiDecryptedIdentity, RhiEncryptedIdentityEnvelopeError,
+ RhiIdentityEnvelopeBinding, RhiRuntimeContext, RhiWrappingCredential,
+ open_rhi_encrypted_identity, resolve_rhi_wrapping_credential,
+};
+
+#[cfg(test)]
+const RUNTIME_ADAPTER_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/runtime_adapters.v1.json");
+
+/// Exact version of the RHI runtime-adapter contract.
+pub const RHI_RUNTIME_ADAPTER_CONTRACT_VERSION: u32 = 1;
+
+/// Largest full-jitter ceiling admitted by the RHI v1 configuration contract.
+pub const RHI_RUNTIME_JITTER_MAX_MILLISECONDS: u64 = 3_600_000;
+
+/// Maximum entropy draws allowed for one exact unbiased full-jitter sample.
+pub const RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS: usize = 16;
+
+/// Stable source-free runtime-adapter failure classification.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiRuntimeAdapterErrorKind {
+ InvalidJitterBound,
+ EntropyUnavailable,
+ WallClockUnavailable,
+ MonotonicDeadlineInvalid,
+ CredentialAccess,
+ IdentityAccess,
+}
+
+impl RhiRuntimeAdapterErrorKind {
+ /// Returns the stable machine-facing safe code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidJitterBound => "runtime_jitter_bound_invalid",
+ Self::EntropyUnavailable => "runtime_entropy_unavailable",
+ Self::WallClockUnavailable => "runtime_wall_clock_unavailable",
+ Self::MonotonicDeadlineInvalid => "runtime_monotonic_deadline_invalid",
+ Self::CredentialAccess => "runtime_credential_access_failed",
+ Self::IdentityAccess => "runtime_identity_access_failed",
+ }
+ }
+
+ const fn message(self) -> &'static str {
+ match self {
+ Self::InvalidJitterBound => "RHI jitter bound is invalid",
+ Self::EntropyUnavailable => "RHI entropy source is unavailable",
+ Self::WallClockUnavailable => "RHI wall clock is unavailable",
+ Self::MonotonicDeadlineInvalid => "RHI monotonic deadline is invalid",
+ Self::CredentialAccess => "RHI credential access failed",
+ Self::IdentityAccess => "RHI identity access failed",
+ }
+ }
+}
+
+/// One redacted source-free runtime-adapter failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiRuntimeAdapterError {
+ kind: RhiRuntimeAdapterErrorKind,
+}
+
+impl RhiRuntimeAdapterError {
+ const fn new(kind: RhiRuntimeAdapterErrorKind) -> Self {
+ Self { kind }
+ }
+
+ /// Returns the stable failure kind.
+ #[must_use]
+ pub const fn kind(self) -> RhiRuntimeAdapterErrorKind {
+ self.kind
+ }
+
+ /// Returns the stable machine-facing safe code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.kind.code()
+ }
+}
+
+impl fmt::Debug for RhiRuntimeAdapterError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiRuntimeAdapterError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl fmt::Display for RhiRuntimeAdapterError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(self.kind.message())
+ }
+}
+
+impl Error for RhiRuntimeAdapterError {}
+
+/// Validated inclusive maximum for one full-jitter sample, in whole milliseconds.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RhiJitterBoundMilliseconds(u64);
+
+impl RhiJitterBoundMilliseconds {
+ /// Validates a whole-millisecond bound against the complete RHI v1 ceiling.
+ pub const fn new(milliseconds: u64) -> Result<Self, RhiRuntimeAdapterError> {
+ if milliseconds > RHI_RUNTIME_JITTER_MAX_MILLISECONDS {
+ Err(RhiRuntimeAdapterError::new(
+ RhiRuntimeAdapterErrorKind::InvalidJitterBound,
+ ))
+ } else {
+ Ok(Self(milliseconds))
+ }
+ }
+
+ /// Returns the inclusive maximum in whole milliseconds.
+ #[must_use]
+ pub const fn get(self) -> u64 {
+ self.0
+ }
+}
+
+/// One injected full-jitter result, in whole milliseconds.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RhiJitterMilliseconds(u64);
+
+impl RhiJitterMilliseconds {
+ /// Returns the sampled value.
+ #[must_use]
+ pub const fn get(self) -> u64 {
+ self.0
+ }
+
+ /// Returns the sampled value as a duration.
+ #[must_use]
+ pub const fn duration(self) -> Duration {
+ Duration::from_millis(self.0)
+ }
+}
+
+/// Injected wall-time, monotonic-time, and entropy capabilities.
+pub struct RhiTimeEntropyAdapters {
+ wall: Arc<dyn WallClock>,
+ monotonic: Arc<dyn MonotonicClock>,
+ entropy: Arc<dyn EntropySource>,
+}
+
+impl RhiTimeEntropyAdapters {
+ /// Owns injected adapters without reading a clock or entropy source.
+ pub fn new<W, M, E>(wall: W, monotonic: M, entropy: E) -> Self
+ where
+ W: WallClock + 'static,
+ M: MonotonicClock + 'static,
+ E: EntropySource + 'static,
+ {
+ Self {
+ wall: Arc::new(wall),
+ monotonic: Arc::new(monotonic),
+ entropy: Arc::new(entropy),
+ }
+ }
+
+ /// Constructs the production adapters without reading any value yet.
+ #[must_use]
+ pub fn system() -> Self {
+ Self::new(SystemWallClock, SystemMonotonicClock::new(), SystemEntropy)
+ }
+
+ /// Reads one explicit whole-second UTC observation.
+ pub fn now_utc(&self) -> Result<UnixTimeSeconds, RhiRuntimeAdapterError> {
+ self.wall.now_utc().map_err(|_| {
+ RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::WallClockUnavailable)
+ })
+ }
+
+ /// Reads one observation from the injected process-local monotonic domain.
+ #[must_use]
+ pub fn now_monotonic(&self) -> MonotonicTime {
+ self.monotonic.now_monotonic()
+ }
+
+ /// Computes a deadline in the injected monotonic domain without wrapping.
+ pub fn deadline_after(
+ &self,
+ duration: Duration,
+ ) -> Result<MonotonicDeadline, RhiRuntimeAdapterError> {
+ self.monotonic.deadline_after(duration).map_err(|_| {
+ RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid)
+ })
+ }
+
+ /// Samples unbiased full jitter in the inclusive range `0..=maximum`.
+ ///
+ /// Rejection sampling is capped so an adversarial injected entropy source
+ /// cannot keep one scheduler decision pending indefinitely.
+ pub fn sample_full_jitter(
+ &self,
+ maximum: RhiJitterBoundMilliseconds,
+ ) -> Result<RhiJitterMilliseconds, RhiRuntimeAdapterError> {
+ let range = maximum.get() + 1;
+ let rejection_threshold = range.wrapping_neg() % range;
+ for _ in 0..RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS {
+ let mut bytes = [0_u8; 8];
+ self.entropy.fill_bytes(&mut bytes).map_err(|_| {
+ RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::EntropyUnavailable)
+ })?;
+ let product = u128::from(u64::from_be_bytes(bytes)) * u128::from(range);
+ let low = u64::try_from(product & u128::from(u64::MAX))
+ .expect("masked multiply-high remainder fits u64");
+ if low >= rejection_threshold {
+ let sampled = u64::try_from(product >> u64::BITS)
+ .expect("multiply-high full-jitter result fits the admitted u64 bound");
+ return Ok(RhiJitterMilliseconds(sampled));
+ }
+ }
+ Err(RhiRuntimeAdapterError::new(
+ RhiRuntimeAdapterErrorKind::EntropyUnavailable,
+ ))
+ }
+}
+
+impl fmt::Debug for RhiTimeEntropyAdapters {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("RhiTimeEntropyAdapters([injected])")
+ }
+}
+
+/// Transport-neutral capabilities for bounded evidence fetch, live subscription, and publication.
+///
+/// Construction performs no network, DNS, or TLS operation. The capabilities
+/// remain sealed inside RHI so concrete transports and detachable I/O handles
+/// do not become public runtime authority.
+pub struct RhiTransportAdapters {
+ _evidence_source: Arc<dyn EventSource>,
+ _evidence_subscriber: Arc<dyn EventSubscriber>,
+ _publication_sink: Arc<dyn EventSink>,
+}
+
+impl RhiTransportAdapters {
+ /// Binds the complete transport-neutral capability inventory without I/O.
+ #[must_use]
+ pub fn new(
+ evidence_source: Arc<dyn EventSource>,
+ evidence_subscriber: Arc<dyn EventSubscriber>,
+ publication_sink: Arc<dyn EventSink>,
+ ) -> Self {
+ Self {
+ _evidence_source: evidence_source,
+ _evidence_subscriber: evidence_subscriber,
+ _publication_sink: publication_sink,
+ }
+ }
+}
+
+impl fmt::Debug for RhiTransportAdapters {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("RhiTransportAdapters([sealed])")
+ }
+}
+
+/// Injected read-existing-only wrapping-credential access.
+pub trait RhiCredentialAccess: Send + Sync {
+ /// Resolves the configured credential for the exact runtime and identity binding.
+ fn resolve_existing(
+ &self,
+ runtime: &RhiRuntimeContext,
+ binding: &RhiIdentityEnvelopeBinding,
+ ) -> Result<RhiWrappingCredential, RhiCredentialResolutionError>;
+}
+
+/// Injected read-existing-only encrypted-identity access.
+pub trait RhiIdentityAccess: Send + Sync {
+ /// Opens and independently verifies the exact configured encrypted identity.
+ fn open_existing(
+ &self,
+ binding: &RhiIdentityEnvelopeBinding,
+ credential: &RhiWrappingCredential,
+ ) -> Result<RhiDecryptedIdentity, RhiEncryptedIdentityEnvelopeError>;
+}
+
+/// Canonical credential resolver backed by the governed instance artifact boundary.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct CanonicalRhiCredentialAccess;
+
+impl RhiCredentialAccess for CanonicalRhiCredentialAccess {
+ fn resolve_existing(
+ &self,
+ runtime: &RhiRuntimeContext,
+ binding: &RhiIdentityEnvelopeBinding,
+ ) -> Result<RhiWrappingCredential, RhiCredentialResolutionError> {
+ resolve_rhi_wrapping_credential(runtime, binding)
+ }
+}
+
+/// Canonical encrypted-identity opener backed by the governed envelope boundary.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct CanonicalRhiIdentityAccess;
+
+impl RhiIdentityAccess for CanonicalRhiIdentityAccess {
+ fn open_existing(
+ &self,
+ binding: &RhiIdentityEnvelopeBinding,
+ credential: &RhiWrappingCredential,
+ ) -> Result<RhiDecryptedIdentity, RhiEncryptedIdentityEnvelopeError> {
+ open_rhi_encrypted_identity(binding, credential)
+ }
+}
+
+/// Ordered credential-then-identity access with no fallback or ambient selector.
+pub struct RhiIdentityCredentialAdapters {
+ credential: Arc<dyn RhiCredentialAccess>,
+ identity: Arc<dyn RhiIdentityAccess>,
+}
+
+impl RhiIdentityCredentialAdapters {
+ /// Owns injected accessors without reading a credential or identity.
+ #[must_use]
+ pub fn new(
+ credential: Arc<dyn RhiCredentialAccess>,
+ identity: Arc<dyn RhiIdentityAccess>,
+ ) -> Self {
+ Self {
+ credential,
+ identity,
+ }
+ }
+
+ /// Constructs the canonical read-existing-only accessors without performing I/O.
+ #[must_use]
+ pub fn canonical() -> Self {
+ Self::new(
+ Arc::new(CanonicalRhiCredentialAccess),
+ Arc::new(CanonicalRhiIdentityAccess),
+ )
+ }
+
+ /// Resolves the credential first, then opens and verifies the identity.
+ pub fn open_existing(
+ &self,
+ runtime: &RhiRuntimeContext,
+ binding: &RhiIdentityEnvelopeBinding,
+ ) -> Result<RhiDecryptedIdentity, RhiRuntimeAdapterError> {
+ let credential = self
+ .credential
+ .resolve_existing(runtime, binding)
+ .map_err(|_| {
+ RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::CredentialAccess)
+ })?;
+ self.identity
+ .open_existing(binding, &credential)
+ .map_err(|_| RhiRuntimeAdapterError::new(RhiRuntimeAdapterErrorKind::IdentityAccess))
+ }
+}
+
+impl fmt::Debug for RhiIdentityCredentialAdapters {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("RhiIdentityCredentialAdapters([sealed])")
+ }
+}
+
+/// Complete injected RHI foundation adapters with privately join-owned tasks.
+///
+/// This value creates no runtime, installs no signal or logger, performs no
+/// transport or identity I/O, and exposes no task handle or supervisor.
+#[must_use = "runtime adapters retain join-owned task authority"]
+pub struct RhiRuntimeAdapters {
+ time_entropy: RhiTimeEntropyAdapters,
+ _transport: RhiTransportAdapters,
+ identity_credential: RhiIdentityCredentialAdapters,
+ supervisor: TaskSupervisor,
+}
+
+impl RhiRuntimeAdapters {
+ /// Composes already-constructed injected capabilities without invoking them.
+ pub fn new(
+ time_entropy: RhiTimeEntropyAdapters,
+ transport: RhiTransportAdapters,
+ identity_credential: RhiIdentityCredentialAdapters,
+ ) -> Self {
+ Self {
+ time_entropy,
+ _transport: transport,
+ identity_credential,
+ supervisor: TaskSupervisor::new(),
+ }
+ }
+
+ /// Returns the injected time and entropy boundary.
+ #[must_use]
+ pub const fn time_entropy(&self) -> &RhiTimeEntropyAdapters {
+ &self.time_entropy
+ }
+
+ /// Returns the ordered identity and credential boundary.
+ #[must_use]
+ pub const fn identity_credential(&self) -> &RhiIdentityCredentialAdapters {
+ &self.identity_credential
+ }
+
+ /// Returns the number of join-owned tasks currently registered.
+ #[must_use]
+ pub fn supervised_task_count(&self) -> usize {
+ self.supervisor.task_count()
+ }
+
+ #[cfg(test)]
+ pub(crate) fn supervisor_mut(&mut self) -> &mut TaskSupervisor {
+ &mut self.supervisor
+ }
+}
+
+impl fmt::Debug for RhiRuntimeAdapters {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiRuntimeAdapters")
+ .field("time_entropy", &"[injected]")
+ .field("transport", &"[sealed]")
+ .field("identity_credential", &"[sealed]")
+ .field("supervised_task_count", &self.supervised_task_count())
+ .finish()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use core::{
+ future::ready,
+ sync::atomic::{AtomicUsize, Ordering},
+ };
+
+ use radroots_service_host::{
+ EntropyError, HostError, MonotonicClockError, TaskClassification, TaskMetadata, TaskName,
+ WallClockError,
+ };
+ use radroots_transport::{
+ BoxFuture, DeliveryReceipt, DeliveryRequest, EventSubscription, FetchPage, FetchRequest,
+ SinkFailure, SinkStatus, SourceStatus, SubscriptionRequest,
+ };
+
+ use super::*;
+
+ #[derive(Clone, Copy)]
+ struct FixedWall(Result<UnixTimeSeconds, WallClockError>);
+
+ impl WallClock for FixedWall {
+ fn now_utc(&self) -> Result<UnixTimeSeconds, WallClockError> {
+ self.0
+ }
+ }
+
+ #[derive(Clone, Copy)]
+ struct FixedMonotonic(MonotonicTime);
+
+ impl MonotonicClock for FixedMonotonic {
+ fn now_monotonic(&self) -> MonotonicTime {
+ self.0
+ }
+ }
+
+ #[derive(Clone, Copy)]
+ struct FixedEntropy(Result<u64, EntropyError>);
+
+ impl EntropySource for FixedEntropy {
+ fn fill_bytes(&self, destination: &mut [u8]) -> Result<(), EntropyError> {
+ let value = self.0?;
+ destination.copy_from_slice(&value.to_be_bytes());
+ Ok(())
+ }
+ }
+
+ struct NoIoTransport;
+
+ impl EventSource for NoIoTransport {
+ fn status(&self) -> BoxFuture<'_, Result<SourceStatus, radroots_transport::Error>> {
+ Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
+ }
+
+ fn fetch(
+ &self,
+ _request: FetchRequest,
+ ) -> BoxFuture<'_, Result<FetchPage, radroots_transport::Error>> {
+ Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
+ }
+ }
+
+ impl EventSubscriber for NoIoTransport {
+ fn subscribe(
+ &self,
+ _request: SubscriptionRequest,
+ ) -> BoxFuture<'_, Result<Box<dyn EventSubscription>, radroots_transport::Error>> {
+ Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
+ }
+ }
+
+ impl EventSink for NoIoTransport {
+ fn status(&self) -> BoxFuture<'_, Result<SinkStatus, radroots_transport::Error>> {
+ Box::pin(ready(Err(radroots_transport::Error::UnsupportedOperation)))
+ }
+
+ fn deliver(
+ &self,
+ request: DeliveryRequest,
+ ) -> BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> {
+ Box::pin(ready(Err(SinkFailure::invalid_contract(&request))))
+ }
+ }
+
+ #[test]
+ fn runtime_adapter_contract_is_exact_and_defers_process_authority() {
+ let contract: serde_json::Value =
+ serde_json::from_str(RUNTIME_ADAPTER_CONTRACT).expect("runtime adapter contract");
+ assert_eq!(
+ contract,
+ serde_json::json!({
+ "schema": "radroots.rhi.runtime-adapters",
+ "schema_version": 1,
+ "contract_version": RHI_RUNTIME_ADAPTER_CONTRACT_VERSION,
+ "time_entropy": {
+ "wall_time": "injected_whole_second_utc",
+ "monotonic_time": "injected_process_local_domain",
+ "entropy": "injected_complete_fill_or_error",
+ "event_authored_time": "untrusted_input"
+ },
+ "jitter": {
+ "algorithm": "rejection_sampled_multiply_high_full_jitter",
+ "unit": "milliseconds",
+ "inclusive_minimum": 0,
+ "inclusive_maximum": RHI_RUNTIME_JITTER_MAX_MILLISECONDS,
+ "maximum_entropy_draws": RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS,
+ "wall_clock_derived": false
+ },
+ "transport": {
+ "contract": "radroots_transport",
+ "evidence_fetch": "EventSource",
+ "evidence_subscription": "EventSubscriber",
+ "publication": "EventSink",
+ "construction_performs_io": false,
+ "concrete_handles_exposed": false
+ },
+ "identity": {
+ "order": ["credential", "encrypted_identity"],
+ "credential": "read_existing_canonical_instance_artifact",
+ "encrypted_identity": "read_existing_and_independently_verify",
+ "fallback": false,
+ "generation": false
+ },
+ "tasks": {
+ "supervisor": "radroots_service_host::TaskSupervisor",
+ "join_owned": true,
+ "handles_exposed": false
+ },
+ "library_exclusions": [
+ "signal_installation",
+ "runtime_creation",
+ "logging_installation",
+ "process_exit",
+ "detached_tasks"
+ ]
+ })
+ );
+ }
+
+ #[test]
+ fn injected_time_deadline_and_full_jitter_are_exactly_bounded() {
+ let now = MonotonicTime::from_duration_since_origin(Duration::from_millis(40));
+ let minimum = RhiTimeEntropyAdapters::new(
+ FixedWall(Ok(UnixTimeSeconds::new(1_000))),
+ FixedMonotonic(now),
+ FixedEntropy(Ok(1)),
+ );
+ assert_eq!(minimum.now_utc().expect("wall").get(), 1_000);
+ assert_eq!(minimum.now_monotonic(), now);
+ assert_eq!(
+ minimum
+ .deadline_after(Duration::from_millis(2))
+ .expect("deadline")
+ .time()
+ .duration_since_origin(),
+ Duration::from_millis(42)
+ );
+ let maximum = RhiJitterBoundMilliseconds::new(RHI_RUNTIME_JITTER_MAX_MILLISECONDS)
+ .expect("maximum bound");
+ assert_eq!(
+ minimum.sample_full_jitter(maximum).expect("minimum").get(),
+ 0
+ );
+
+ let upper = RhiTimeEntropyAdapters::new(
+ FixedWall(Ok(UnixTimeSeconds::new(1))),
+ FixedMonotonic(now),
+ FixedEntropy(Ok(u64::MAX)),
+ );
+ assert_eq!(
+ upper.sample_full_jitter(maximum).expect("maximum").get(),
+ maximum.get()
+ );
+ assert_eq!(
+ upper
+ .sample_full_jitter(RhiJitterBoundMilliseconds::new(0).expect("zero"))
+ .expect("zero sample")
+ .duration(),
+ Duration::ZERO
+ );
+ assert_eq!(
+ RhiJitterBoundMilliseconds::new(RHI_RUNTIME_JITTER_MAX_MILLISECONDS + 1)
+ .expect_err("above maximum")
+ .kind(),
+ RhiRuntimeAdapterErrorKind::InvalidJitterBound
+ );
+
+ let rejected = RhiTimeEntropyAdapters::new(
+ FixedWall(Ok(UnixTimeSeconds::new(1))),
+ FixedMonotonic(now),
+ FixedEntropy(Ok(0)),
+ );
+ assert_eq!(
+ rejected
+ .sample_full_jitter(RhiJitterBoundMilliseconds::new(2).expect("bound"))
+ .expect_err("bounded rejection")
+ .kind(),
+ RhiRuntimeAdapterErrorKind::EntropyUnavailable
+ );
+ }
+
+ #[test]
+ fn injected_failures_and_deadline_overflow_are_stable_and_source_free() {
+ let maximum_time = MonotonicTime::from_duration_since_origin(Duration::MAX);
+ let adapters = RhiTimeEntropyAdapters::new(
+ FixedWall(Err(WallClockError::BeforeUnixEpoch)),
+ FixedMonotonic(maximum_time),
+ FixedEntropy(Err(EntropyError::Unavailable)),
+ );
+ let cases = [
+ (
+ adapters.now_utc().expect_err("wall").kind(),
+ RhiRuntimeAdapterErrorKind::WallClockUnavailable,
+ ),
+ (
+ adapters
+ .deadline_after(Duration::from_millis(1))
+ .expect_err("deadline")
+ .kind(),
+ RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid,
+ ),
+ (
+ adapters
+ .sample_full_jitter(RhiJitterBoundMilliseconds::new(1).expect("bound"))
+ .expect_err("entropy")
+ .kind(),
+ RhiRuntimeAdapterErrorKind::EntropyUnavailable,
+ ),
+ ];
+ for (actual, expected) in cases {
+ assert_eq!(actual, expected);
+ let error = RhiRuntimeAdapterError::new(actual);
+ assert!(Error::source(&error).is_none());
+ assert!(!format!("{error:?} {error}").contains("secret"));
+ }
+ assert_eq!(
+ maximum_time.checked_deadline_after(Duration::from_millis(1)),
+ Err(MonotonicClockError::DeadlineOverflow)
+ );
+ }
+
+ #[tokio::test]
+ async fn adapter_set_is_inert_until_invoked_and_owns_joined_tasks() {
+ let transport = Arc::new(NoIoTransport);
+ let transports = RhiTransportAdapters::new(transport.clone(), transport.clone(), transport);
+ let mut adapters = RhiRuntimeAdapters::new(
+ RhiTimeEntropyAdapters::new(
+ FixedWall(Ok(UnixTimeSeconds::new(1))),
+ FixedMonotonic(MonotonicTime::from_duration_since_origin(Duration::ZERO)),
+ FixedEntropy(Ok(1)),
+ ),
+ transports,
+ RhiIdentityCredentialAdapters::canonical(),
+ );
+ assert_eq!(adapters.supervised_task_count(), 0);
+ let calls = Arc::new(AtomicUsize::new(0));
+ let task_calls = Arc::clone(&calls);
+ adapters
+ .supervisor_mut()
+ .spawn(
+ TaskMetadata::new(
+ TaskName::new("adapter_contract_test").expect("task name"),
+ TaskClassification::OneShot,
+ None,
+ )
+ .expect("metadata"),
+ move |_cancel| async move {
+ task_calls.fetch_add(1, Ordering::Relaxed);
+ Ok::<(), HostError>(())
+ },
+ )
+ .expect("register");
+ assert_eq!(adapters.supervised_task_count(), 1);
+ assert_eq!(
+ adapters
+ .supervisor_mut()
+ .supervise()
+ .await
+ .expect("joined")
+ .len(),
+ 1
+ );
+ assert_eq!(calls.load(Ordering::Relaxed), 1);
+ assert_eq!(adapters.supervised_task_count(), 0);
+ assert_eq!(
+ format!("{adapters:?}"),
+ "RhiRuntimeAdapters { time_entropy: \"[injected]\", transport: \"[sealed]\", identity_credential: \"[sealed]\", supervised_task_count: 0 }"
+ );
+ }
+
+ #[test]
+ fn all_error_codes_messages_and_debug_are_stable() {
+ let cases = [
+ (
+ RhiRuntimeAdapterErrorKind::InvalidJitterBound,
+ "runtime_jitter_bound_invalid",
+ "RHI jitter bound is invalid",
+ ),
+ (
+ RhiRuntimeAdapterErrorKind::EntropyUnavailable,
+ "runtime_entropy_unavailable",
+ "RHI entropy source is unavailable",
+ ),
+ (
+ RhiRuntimeAdapterErrorKind::WallClockUnavailable,
+ "runtime_wall_clock_unavailable",
+ "RHI wall clock is unavailable",
+ ),
+ (
+ RhiRuntimeAdapterErrorKind::MonotonicDeadlineInvalid,
+ "runtime_monotonic_deadline_invalid",
+ "RHI monotonic deadline is invalid",
+ ),
+ (
+ RhiRuntimeAdapterErrorKind::CredentialAccess,
+ "runtime_credential_access_failed",
+ "RHI credential access failed",
+ ),
+ (
+ RhiRuntimeAdapterErrorKind::IdentityAccess,
+ "runtime_identity_access_failed",
+ "RHI identity access failed",
+ ),
+ ];
+ for (kind, code, message) in cases {
+ let error = RhiRuntimeAdapterError::new(kind);
+ assert_eq!(error.code(), code);
+ assert_eq!(error.to_string(), message);
+ assert_eq!(
+ format!("{error:?}"),
+ format!("RhiRuntimeAdapterError {{ kind: {kind:?} }}")
+ );
+ assert!(Error::source(&error).is_none());
+ }
+ }
+}
diff --git a/tests/build_policy.rs b/tests/build_policy.rs
@@ -69,6 +69,13 @@ fn shared_storage_generation_type_is_exactly_source_locked() {
}
#[test]
+fn shared_transport_spi_is_exactly_source_locked_without_serde() {
+ assert!(MANIFEST.contains(
+ "radroots_transport = { git = \"https://github.com/radrootslabs/lib\", rev = \"7d7b454b4c9ed86569671993bd03ca868b676665\", version = \"=0.1.0-alpha\", default-features = false, features = [\"std\"] }"
+ ));
+}
+
+#[test]
fn source_lock_binds_the_current_cargo_lock() {
let digest = lower_hex(&Sha256::digest(include_bytes!("../Cargo.lock")));
assert!(SOURCE_LOCK.starts_with(
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -7,6 +7,9 @@ const ROOT: &str = include_str!("../src/lib.rs");
const ADAPTERS: &str = include_str!("../src/adapters/mod.rs");
const NOSTR_ADAPTERS: &str = include_str!("../src/adapters/nostr/mod.rs");
const FEATURES: &str = include_str!("../src/features/mod.rs");
+const RUNTIME_ADAPTERS: &str = include_str!("../src/runtime_adapters.rs");
+const RUNTIME_ADAPTER_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/runtime_adapters.v1.json");
const PUBLIC_API: &str = include_str!("../contracts/api_baselines/rhi.txt");
const SOURCES: &[&str] = &[
include_str!("../src/adapters/nostr/event.rs"),
@@ -16,6 +19,7 @@ const SOURCES: &[&str] = &[
include_str!("../src/identity_credential.rs"),
include_str!("../src/identity_envelope.rs"),
include_str!("../src/runtime_context.rs"),
+ include_str!("../src/runtime_adapters.rs"),
include_str!("../src/state_catalog.rs"),
include_str!("../src/state_host.rs"),
include_str!("../src/state_maintenance.rs"),
@@ -44,20 +48,26 @@ fn package_identity_is_standalone_and_non_publishable() {
}
#[test]
-fn shared_host_implementations_do_not_escape_the_public_api() {
+fn shared_runtime_contracts_are_curated_without_exposing_implementation_authority() {
for forbidden in [
- "pub use radroots_service_host",
"pub use radroots_service_sqlite",
"pub mod service_host",
"pub mod service_sqlite",
"sqlx::Pool",
"sqlx::SqliteConnection",
+ "SystemEntropy",
+ "SystemMonotonicClock",
+ "SystemWallClock",
+ "TaskSupervisor",
+ "CancellationToken",
] {
assert!(
!ROOT.contains(forbidden),
"RHI public root exposes private host implementation {forbidden}"
);
}
+ assert!(!PUBLIC_API.contains("radroots_service_host::HostError"));
+ assert!(!PUBLIC_API.contains("radroots_service_host::TaskSupervisor"));
}
#[test]
@@ -70,6 +80,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"identity_credential",
"identity_envelope",
"runtime_context",
+ "runtime_adapters",
"state_catalog",
"state_host",
"state_maintenance",
@@ -97,6 +108,17 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
"rhi_schema_catalog",
"validate_rhi_state_catalogs",
"RhiStateCatalogError",
+ "RhiRuntimeAdapters",
+ "RhiTimeEntropyAdapters",
+ "RhiTransportAdapters",
+ "RhiCredentialAccess",
+ "RhiIdentityAccess",
+ "WallClock",
+ "MonotonicClock",
+ "EntropySource",
+ "EntropyError",
+ "WallClockError",
+ "MonotonicClockError",
] {
assert!(
ROOT.contains(required),
@@ -113,6 +135,7 @@ fn state_catalog_module_is_private_and_root_api_is_curated() {
assert!(PUBLIC_API.contains("pub struct rhi::TradeAgreementAttestationError"));
assert!(!PUBLIC_API.contains("rhi::adapters::"));
assert!(!PUBLIC_API.contains("rhi::features::"));
+ assert!(!PUBLIC_API.contains("rhi::runtime_adapters::"));
}
#[test]
@@ -147,7 +170,74 @@ fn public_errors_are_crate_owned_redacted_and_source_free() {
.lines()
.filter(|line| line.starts_with("pub struct rhi::") && line.ends_with("Error"))
.count();
- assert_eq!(public_error_count, 10);
+ assert_eq!(public_error_count, 11);
+}
+
+#[test]
+fn runtime_adapter_boundary_is_exact_bounded_and_process_neutral() {
+ let contract: serde_json::Value =
+ serde_json::from_str(RUNTIME_ADAPTER_CONTRACT).expect("runtime adapter contract");
+ assert_eq!(contract["schema"], "radroots.rhi.runtime-adapters");
+ assert_eq!(contract["schema_version"], 1);
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["jitter"]["inclusive_maximum"], 3_600_000);
+ assert_eq!(contract["jitter"]["maximum_entropy_draws"], 16);
+ assert_eq!(contract["jitter"]["wall_clock_derived"], false);
+ assert_eq!(contract["transport"]["evidence_fetch"], "EventSource");
+ assert_eq!(
+ contract["transport"]["evidence_subscription"],
+ "EventSubscriber"
+ );
+ assert_eq!(contract["transport"]["publication"], "EventSink");
+ assert_eq!(
+ contract["identity"]["order"],
+ serde_json::json!(["credential", "encrypted_identity"])
+ );
+ assert_eq!(contract["identity"]["fallback"], false);
+ assert_eq!(contract["identity"]["generation"], false);
+ assert_eq!(contract["tasks"]["join_owned"], true);
+ assert_eq!(contract["tasks"]["handles_exposed"], false);
+
+ for required in [
+ "Arc<dyn WallClock>",
+ "Arc<dyn MonotonicClock>",
+ "Arc<dyn EntropySource>",
+ "Arc<dyn EventSource>",
+ "Arc<dyn EventSubscriber>",
+ "Arc<dyn EventSink>",
+ "TaskSupervisor",
+ "RHI_RUNTIME_JITTER_MAX_MILLISECONDS: u64 = 3_600_000",
+ "RHI_RUNTIME_JITTER_MAX_ENTROPY_DRAWS: usize = 16",
+ "u128::from(u64::from_be_bytes(bytes)) * u128::from(range)",
+ "low >= rejection_threshold",
+ "resolve_rhi_wrapping_credential(runtime, binding)",
+ "open_rhi_encrypted_identity(binding, credential)",
+ ] {
+ assert!(
+ RUNTIME_ADAPTERS.contains(required),
+ "runtime adapter boundary is missing {required}"
+ );
+ }
+ for forbidden in [
+ "tokio::runtime::Runtime",
+ "tokio::runtime::Builder",
+ "tokio::signal",
+ "signal_hook",
+ "tracing_subscriber",
+ "std::process::exit",
+ "tokio::spawn",
+ "std::thread::spawn",
+ "SystemTime::now",
+ "subsec_nanos",
+ "rand::",
+ ] {
+ assert!(
+ !RUNTIME_ADAPTERS.contains(forbidden),
+ "runtime adapter boundary contains forbidden authority {forbidden}"
+ );
+ }
+ assert!(MANIFEST.contains("radroots_transport ="));
+ assert!(MANIFEST.contains("default-features = false, features = [\"std\"]"));
}
#[test]
@@ -156,8 +246,19 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"## Public API boundary",
"one curated crate-root API",
"public errors use RHI-owned stable classifications",
+ "shared clock and entropy traits and their source-free error values",
+ "failures into stable RHI classifications",
"```compile_fail",
"[RHI API baseline](contracts/api_baselines/rhi.txt)",
+ "## Injected runtime adapters",
+ "whole-second wall UTC",
+ "process-local monotonic time",
+ "exact v1 maximum of 3,600,000",
+ "fails closed after sixteen rejected entropy draws",
+ "never derived from wall-clock",
+ "Constructing the adapter set performs no clock read",
+ "no signal handler, Tokio runtime, logger, or process-exit policy",
+ "[`runtime_adapters.v1.json`](contracts/services_hardening/runtime_adapters.v1.json)",
] {
assert!(README.contains(required), "README is missing {required}");
}
@@ -166,6 +267,8 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() {
"contracts/api_baselines/rhi.txt",
"Public errors must use RHI-owned stable classifications",
"no raw dependency-owned source chain",
+ "Compose those dependencies only through the sealed runtime-adapter boundary",
+ "exposes no task handle or concrete transport handle",
] {
assert!(AGENTS.contains(required), "AGENTS is missing {required}");
}