lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 78de9512faa5af442a703c5736a590b8ad6cd542
parent b57c36486e44f6b4ed9ad6e754e6fca0a9ceea11
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 00:55:41 +0000

service-host: add deterministic build info

- Add validated timestamp-free source and contract identity metadata.
- Fail closed on missing or placeholder metadata in release builds.
- Project exact build fields into the frozen service-status contract.
- Verify deterministic snapshots, malformed input, and release-mode behavior.

Diffstat:
MCargo.lock | 2++
Mcrates/service_host/Cargo.toml | 4++++
Acrates/service_host/src/build_info.rs | 543+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/service_host/src/lib.rs | 32++++++++++++++++++++++++++++++++
Mcrates/service_host/tests/package_boundary.rs | 7+++++--
5 files changed, 586 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3880,6 +3880,8 @@ name = "radroots_service_host" version = "0.1.0-alpha" dependencies = [ "getrandom 0.2.17", + "serde", + "serde_json", ] [[package]] diff --git a/crates/service_host/Cargo.toml b/crates/service_host/Cargo.toml @@ -13,6 +13,10 @@ readme = "README.md" [dependencies] getrandom = { workspace = true } +serde = { workspace = true, features = ["derive", "std"] } + +[dev-dependencies] +serde_json = { workspace = true } [lints] workspace = true diff --git a/crates/service_host/src/build_info.rs b/crates/service_host/src/build_info.rs @@ -0,0 +1,543 @@ +//! Deterministic build identity shared by hardened services. + +use core::fmt; + +use serde::Serialize; + +/// Environment variable containing the consuming service's exact source revision. +pub const SERVICE_REVISION_ENV: &str = "RADROOTS_SERVICE_REVISION"; +/// Environment variable containing the exact Radroots Lib revision. +pub const LIB_REVISION_ENV: &str = "RADROOTS_LIB_REVISION"; +/// Environment variable containing the exact governed Rust version. +pub const RUST_VERSION_ENV: &str = "RADROOTS_RUST_VERSION"; +/// Environment variable containing the exact Rust target triple. +pub const BUILD_TARGET_ENV: &str = "RADROOTS_BUILD_TARGET"; + +/// Maximum byte length of service versions, Rust versions, targets, and feature profiles. +pub const BUILD_INFO_TEXT_MAX_BYTES: usize = 128; +const DEVELOPMENT_REVISION: &str = "0000000000000000000000000000000000000000"; +const DEVELOPMENT_VALUE: &str = "development"; + +/// Determines whether missing compile-time metadata is an error. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BuildMode { + Development, + Release, +} + +/// Exact versions for every service-host contract family. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ContractVersions { + config: u32, + state: u32, + admin: u32, + status: u32, + provider: u32, +} + +impl ContractVersions { + /// Creates a contract-version cohort, rejecting zero as an unpublished identity. + pub fn new( + config: u32, + state: u32, + admin: u32, + status: u32, + provider: u32, + ) -> Result<Self, BuildInfoError> { + let versions = Self { + config, + state, + admin, + status, + provider, + }; + if [config, state, admin, status, provider].contains(&0) { + return Err(BuildInfoError::InvalidValue( + BuildInfoField::ContractVersion, + )); + } + Ok(versions) + } + + #[must_use] + pub const fn config(self) -> u32 { + self.config + } + + #[must_use] + pub const fn state(self) -> u32 { + self.state + } + + #[must_use] + pub const fn admin(self) -> u32 { + self.admin + } + + #[must_use] + pub const fn status(self) -> u32 { + self.status + } + + #[must_use] + pub const fn provider(self) -> u32 { + self.provider + } +} + +/// Compile-time strings captured by [`crate::compile_time_build_info!`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BuildInfoEnvironment<'a> { + pub service_version: Option<&'a str>, + pub service_commit: Option<&'a str>, + pub lib_revision: Option<&'a str>, + pub rust_version: Option<&'a str>, + pub target: Option<&'a str>, + pub feature_profile: Option<&'a str>, + pub contract_versions: ContractVersions, +} + +/// A deterministic, timestamp-free service build identity. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct BuildInfo { + service_version: String, + service_commit: String, + lib_revision: String, + rust_version: String, + target: String, + feature_profile: String, + contract_versions: ContractVersions, +} + +/// The exact build-information projection frozen by the service status contracts. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct StatusBuildInfo<'a> { + version: &'a str, + revision: &'a str, + toolchain: &'a str, + contract_versions: ContractVersions, +} + +impl BuildInfo { + /// Validates captured compile-time values and constructs one stable build identity. + pub fn from_compile_time( + mode: BuildMode, + environment: BuildInfoEnvironment<'_>, + ) -> Result<Self, BuildInfoError> { + let service_version = required_text( + mode, + environment.service_version, + "CARGO_PKG_VERSION", + BuildInfoField::ServiceVersion, + )?; + let service_commit = required_revision( + mode, + environment.service_commit, + SERVICE_REVISION_ENV, + BuildInfoField::ServiceCommit, + )?; + let lib_revision = required_revision( + mode, + environment.lib_revision, + LIB_REVISION_ENV, + BuildInfoField::LibRevision, + )?; + let rust_version = required_text( + mode, + environment.rust_version, + RUST_VERSION_ENV, + BuildInfoField::RustVersion, + )?; + let target = required_text( + mode, + environment.target, + BUILD_TARGET_ENV, + BuildInfoField::Target, + )?; + let feature_profile = required_text( + mode, + environment.feature_profile, + "feature_profile", + BuildInfoField::FeatureProfile, + )?; + + Ok(Self { + service_version, + service_commit, + lib_revision, + rust_version, + target, + feature_profile, + contract_versions: environment.contract_versions, + }) + } + + #[must_use] + pub fn service_version(&self) -> &str { + &self.service_version + } + + #[must_use] + pub fn service_commit(&self) -> &str { + &self.service_commit + } + + #[must_use] + pub fn lib_revision(&self) -> &str { + &self.lib_revision + } + + #[must_use] + pub fn rust_version(&self) -> &str { + &self.rust_version + } + + #[must_use] + pub fn target(&self) -> &str { + &self.target + } + + #[must_use] + pub fn feature_profile(&self) -> &str { + &self.feature_profile + } + + #[must_use] + pub const fn contract_versions(&self) -> ContractVersions { + self.contract_versions + } + + /// Projects the complete identity into the frozen Myc/RHI status shape. + /// + /// The full Lib revision, target, and feature profile remain available on `BuildInfo` and are + /// bound by source-lock verification; the status contract intentionally exposes only these + /// four fields. + #[must_use] + pub fn status_projection(&self) -> StatusBuildInfo<'_> { + StatusBuildInfo { + version: &self.service_version, + revision: &self.service_commit, + toolchain: &self.rust_version, + contract_versions: self.contract_versions, + } + } +} + +/// Identifies the invalid field without retaining rejected input. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BuildInfoField { + ServiceVersion, + ServiceCommit, + LibRevision, + RustVersion, + Target, + FeatureProfile, + ContractVersion, +} + +impl fmt::Display for BuildInfoField { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::ServiceVersion => "service_version", + Self::ServiceCommit => "service_commit", + Self::LibRevision => "lib_revision", + Self::RustVersion => "rust_version", + Self::Target => "target", + Self::FeatureProfile => "feature_profile", + Self::ContractVersion => "contract_version", + }) + } +} + +/// Validation failure for deterministic build metadata. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BuildInfoError { + MissingVariable(&'static str), + InvalidValue(BuildInfoField), +} + +impl fmt::Display for BuildInfoError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingVariable(variable) => { + write!(formatter, "required build variable {variable} is missing") + } + Self::InvalidValue(field) => write!(formatter, "build field {field} is invalid"), + } + } +} + +impl std::error::Error for BuildInfoError {} + +fn required_text( + mode: BuildMode, + value: Option<&str>, + variable: &'static str, + field: BuildInfoField, +) -> Result<String, BuildInfoError> { + match value { + Some(value) if valid_text(value) => Ok(value.to_owned()), + Some(_) => Err(BuildInfoError::InvalidValue(field)), + None if mode == BuildMode::Development => Ok(DEVELOPMENT_VALUE.to_owned()), + None => Err(BuildInfoError::MissingVariable(variable)), + } +} + +fn required_revision( + mode: BuildMode, + value: Option<&str>, + variable: &'static str, + field: BuildInfoField, +) -> Result<String, BuildInfoError> { + match value { + Some(value) + if valid_revision(value) + && (mode != BuildMode::Release || value != DEVELOPMENT_REVISION) => + { + Ok(value.to_owned()) + } + Some(_) => Err(BuildInfoError::InvalidValue(field)), + None if mode == BuildMode::Development => Ok(DEVELOPMENT_REVISION.to_owned()), + None => Err(BuildInfoError::MissingVariable(variable)), + } +} + +fn valid_text(value: &str) -> bool { + let mut bytes = value.bytes(); + let Some(first) = bytes.next() else { + return false; + }; + value.len() <= BUILD_INFO_TEXT_MAX_BYTES + && first.is_ascii_alphanumeric() + && bytes + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) +} + +fn valid_revision(value: &str) -> bool { + value.len() == 40 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SERVICE_REVISION: &str = "0123456789abcdef0123456789abcdef01234567"; + const LIB_REVISION: &str = "89abcdef0123456789abcdef0123456789abcdef"; + + fn contracts() -> ContractVersions { + ContractVersions::new(1, 2, 3, 4, 5).expect("contract versions") + } + + fn complete_environment() -> BuildInfoEnvironment<'static> { + BuildInfoEnvironment { + service_version: Some("0.1.0-alpha"), + service_commit: Some(SERVICE_REVISION), + lib_revision: Some(LIB_REVISION), + rust_version: Some("1.97.1"), + target: Some("x86_64-unknown-linux-gnu"), + feature_profile: Some("service-host"), + contract_versions: contracts(), + } + } + + #[test] + fn serialization_is_an_exact_timestamp_free_snapshot() { + let build = BuildInfo::from_compile_time(BuildMode::Release, complete_environment()) + .expect("valid build information"); + let json = serde_json::to_string_pretty(&build).expect("serialize build information"); + + assert_eq!( + json, + r#"{ + "service_version": "0.1.0-alpha", + "service_commit": "0123456789abcdef0123456789abcdef01234567", + "lib_revision": "89abcdef0123456789abcdef0123456789abcdef", + "rust_version": "1.97.1", + "target": "x86_64-unknown-linux-gnu", + "feature_profile": "service-host", + "contract_versions": { + "config": 1, + "state": 2, + "admin": 3, + "status": 4, + "provider": 5 + } +}"# + ); + assert!(!json.contains("time")); + assert!(!json.contains("date")); + + let status_json = serde_json::to_string_pretty(&build.status_projection()) + .expect("serialize status build information"); + assert_eq!( + status_json, + r#"{ + "version": "0.1.0-alpha", + "revision": "0123456789abcdef0123456789abcdef01234567", + "toolchain": "1.97.1", + "contract_versions": { + "config": 1, + "state": 2, + "admin": 3, + "status": 4, + "provider": 5 + } +}"# + ); + } + + #[test] + fn release_mode_fails_closed_for_every_missing_build_variable() { + let complete = complete_environment(); + let cases = [ + ( + BuildInfoEnvironment { + service_version: None, + ..complete + }, + "CARGO_PKG_VERSION", + ), + ( + BuildInfoEnvironment { + service_commit: None, + ..complete + }, + SERVICE_REVISION_ENV, + ), + ( + BuildInfoEnvironment { + lib_revision: None, + ..complete + }, + LIB_REVISION_ENV, + ), + ( + BuildInfoEnvironment { + rust_version: None, + ..complete + }, + RUST_VERSION_ENV, + ), + ( + BuildInfoEnvironment { + target: None, + ..complete + }, + BUILD_TARGET_ENV, + ), + ( + BuildInfoEnvironment { + feature_profile: None, + ..complete + }, + "feature_profile", + ), + ]; + + for (environment, variable) in cases { + assert_eq!( + BuildInfo::from_compile_time(BuildMode::Release, environment), + Err(BuildInfoError::MissingVariable(variable)) + ); + } + } + + #[test] + fn identical_inputs_are_equal_and_development_fallbacks_are_stable() { + let left = BuildInfo::from_compile_time(BuildMode::Release, complete_environment()) + .expect("left build information"); + let right = BuildInfo::from_compile_time(BuildMode::Release, complete_environment()) + .expect("right build information"); + assert_eq!(left, right); + + let development = BuildInfo::from_compile_time( + BuildMode::Development, + BuildInfoEnvironment { + service_version: None, + service_commit: None, + lib_revision: None, + rust_version: None, + target: None, + feature_profile: None, + contract_versions: contracts(), + }, + ) + .expect("development fallbacks"); + assert_eq!(development.service_version(), DEVELOPMENT_VALUE); + assert_eq!(development.service_commit(), DEVELOPMENT_REVISION); + assert_eq!(development.lib_revision(), DEVELOPMENT_REVISION); + assert_eq!(development.rust_version(), DEVELOPMENT_VALUE); + assert_eq!(development.target(), DEVELOPMENT_VALUE); + assert_eq!(development.feature_profile(), DEVELOPMENT_VALUE); + } + + #[test] + fn malformed_values_and_zero_contract_versions_are_rejected() { + for revision in [ + "short", + "ABCDEF0123456789abcdef0123456789abcdef01", + DEVELOPMENT_REVISION, + ] { + let mut environment = complete_environment(); + environment.service_commit = Some(revision); + assert_eq!( + BuildInfo::from_compile_time(BuildMode::Release, environment), + Err(BuildInfoError::InvalidValue(BuildInfoField::ServiceCommit)) + ); + + let mut environment = complete_environment(); + environment.lib_revision = Some(revision); + assert_eq!( + BuildInfo::from_compile_time(BuildMode::Release, environment), + Err(BuildInfoError::InvalidValue(BuildInfoField::LibRevision)) + ); + } + assert_eq!( + ContractVersions::new(1, 1, 0, 1, 1), + Err(BuildInfoError::InvalidValue( + BuildInfoField::ContractVersion + )) + ); + + for invalid in ["bad version", " rustc", ".1.97.1", "rustc/+nightly"] { + let mut environment = complete_environment(); + environment.service_version = Some(invalid); + assert_eq!( + BuildInfo::from_compile_time(BuildMode::Release, environment), + Err(BuildInfoError::InvalidValue(BuildInfoField::ServiceVersion)) + ); + + let mut environment = complete_environment(); + environment.rust_version = Some(invalid); + assert_eq!( + BuildInfo::from_compile_time(BuildMode::Release, environment), + Err(BuildInfoError::InvalidValue(BuildInfoField::RustVersion)) + ); + } + } + + #[test] + fn macro_captures_the_consuming_crate_version() { + let result = crate::compile_time_build_info!( + feature_profile: "service-host", + contract_versions: contracts(), + ); + + if cfg!(debug_assertions) { + let build = result.expect("debug builds accept missing release variables"); + assert_eq!(build.service_version(), env!("CARGO_PKG_VERSION")); + assert_eq!(build.contract_versions(), contracts()); + } else { + assert_eq!( + result, + Err(BuildInfoError::MissingVariable(SERVICE_REVISION_ENV)) + ); + } + } +} diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs @@ -2,13 +2,45 @@ //! Reusable, service-neutral host mechanics for Radroots services. +pub mod build_info; pub mod entropy; pub mod error; pub mod time; +pub use build_info::{ + BuildInfo, BuildInfoEnvironment, BuildInfoError, BuildInfoField, BuildMode, ContractVersions, + StatusBuildInfo, +}; pub use entropy::{EntropyError, EntropySource, SystemEntropy}; pub use error::{HostError, HostErrorCode, HostErrorKind, SafeHostError}; pub use time::{ MonotonicClock, MonotonicClockError, MonotonicDeadline, MonotonicTime, SystemMonotonicClock, SystemWallClock, UnixTimeSeconds, WallClock, WallClockError, }; + +/// Constructs validated build information from the consuming service's compile-time environment. +/// +/// Release builds fail closed when any governed build variable is absent. Debug builds use stable +/// development placeholders so ordinary local development does not require release orchestration. +#[macro_export] +macro_rules! compile_time_build_info { + (feature_profile: $feature_profile:expr, contract_versions: $contract_versions:expr $(,)?) => {{ + let mode = if cfg!(debug_assertions) { + $crate::BuildMode::Development + } else { + $crate::BuildMode::Release + }; + $crate::BuildInfo::from_compile_time( + mode, + $crate::BuildInfoEnvironment { + service_version: Some(env!("CARGO_PKG_VERSION")), + service_commit: option_env!("RADROOTS_SERVICE_REVISION"), + lib_revision: option_env!("RADROOTS_LIB_REVISION"), + rust_version: option_env!("RADROOTS_RUST_VERSION"), + target: option_env!("RADROOTS_BUILD_TARGET"), + feature_profile: Some($feature_profile), + contract_versions: $contract_versions, + }, + ) + }}; +} diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs @@ -17,10 +17,13 @@ fn service_host_is_unpublished_lint_governed_and_dependency_bounded() { ); } - assert_eq!(dependency_keys(MANIFEST), BTreeSet::from(["getrandom"])); + assert_eq!( + dependency_keys(MANIFEST), + BTreeSet::from(["getrandom", "serde"]) + ); assert_eq!( public_modules(ROOT), - BTreeSet::from(["entropy", "error", "time"]) + BTreeSet::from(["build_info", "entropy", "error", "time"]) ); }