commit 301daed1f3e3327727e475723cfe58dda347a7ac
parent 50d1205908091438de40e5b13fa636e78191c458
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 07:43:32 +0000
runtime-paths: add runtime context
- seal resolved service identity, profile, and canonical paths in one immutable context
- constrain bootstrap provenance to a closed vocabulary with selector cross-binding
- redact path-bearing bootstrap and context diagnostics and serialization
- cover equality, source projection, no-I/O construction, and forgery rejection
Diffstat:
2 files changed, 495 insertions(+), 0 deletions(-)
diff --git a/crates/runtime_paths/src/context.rs b/crates/runtime_paths/src/context.rs
@@ -0,0 +1,490 @@
+//! Immutable resolved bootstrap context for one service instance.
+
+use core::fmt;
+use std::path::PathBuf;
+
+use serde::{Serialize, Serializer, ser::SerializeStruct};
+use thiserror::Error;
+
+use crate::{
+ InstanceId, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsServiceInstancePaths, ServiceId, default_service_instance_paths,
+};
+
+/// Closed provenance vocabulary for effective runtime configuration.
+///
+/// Arbitrary strings, secrets, and high-cardinality labels cannot be converted
+/// into this vocabulary:
+///
+/// ```compile_fail
+/// use radroots_runtime_paths::RuntimeContextSource;
+///
+/// let _ = RuntimeContextSource::from("secret:caller-controlled-value");
+/// ```
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum RuntimeContextSource {
+ BootstrapCli,
+ Toml,
+ SafeDefault,
+ DerivedPath,
+}
+
+/// Sealed validated bootstrap input for one runtime context.
+#[derive(Clone, PartialEq, Eq)]
+pub struct RuntimeContextBootstrap {
+ profile: RadrootsPathProfile,
+ repo_local_root: Option<PathBuf>,
+ profile_source: RuntimeContextSource,
+ instance_source: RuntimeContextSource,
+}
+
+impl fmt::Debug for RuntimeContextBootstrap {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RuntimeContextBootstrap")
+ .field("profile", &self.profile)
+ .field(
+ "repo_local_root",
+ &self.repo_local_root.as_ref().map(|_| "[redacted]"),
+ )
+ .field("profile_source", &self.profile_source)
+ .field("instance_source", &self.instance_source)
+ .finish()
+ }
+}
+
+impl RuntimeContextBootstrap {
+ pub fn new(
+ profile: RadrootsPathProfile,
+ repo_local_root: Option<PathBuf>,
+ profile_source: RuntimeContextSource,
+ instance_source: RuntimeContextSource,
+ ) -> Result<Self, RuntimeContextError> {
+ if matches!(profile, RadrootsPathProfile::MobileNative)
+ || (matches!(profile, RadrootsPathProfile::RepoLocal)
+ && !matches!(profile_source, RuntimeContextSource::BootstrapCli))
+ || !matches!(
+ profile_source,
+ RuntimeContextSource::BootstrapCli | RuntimeContextSource::SafeDefault
+ )
+ || !matches!(
+ instance_source,
+ RuntimeContextSource::BootstrapCli | RuntimeContextSource::SafeDefault
+ )
+ || (matches!(profile, RadrootsPathProfile::RepoLocal) != repo_local_root.is_some())
+ {
+ return Err(RuntimeContextError::InvalidBootstrapBinding);
+ }
+ Ok(Self {
+ profile,
+ repo_local_root,
+ profile_source,
+ instance_source,
+ })
+ }
+
+ #[must_use]
+ pub fn profile(&self) -> RadrootsPathProfile {
+ self.profile
+ }
+}
+
+/// Closed provenance bound to every runtime-context field class.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+pub struct RuntimeContextSources {
+ service: RuntimeContextSource,
+ instance: RuntimeContextSource,
+ profile: RuntimeContextSource,
+ repo_local_root: Option<RuntimeContextSource>,
+ paths: RuntimeContextSource,
+}
+
+impl RuntimeContextSources {
+ #[must_use]
+ pub fn service(&self) -> RuntimeContextSource {
+ self.service
+ }
+
+ #[must_use]
+ pub fn instance(&self) -> RuntimeContextSource {
+ self.instance
+ }
+
+ #[must_use]
+ pub fn profile(&self) -> RuntimeContextSource {
+ self.profile
+ }
+
+ #[must_use]
+ pub fn repo_local_root(&self) -> Option<RuntimeContextSource> {
+ self.repo_local_root
+ }
+
+ #[must_use]
+ pub fn paths(&self) -> RuntimeContextSource {
+ self.paths
+ }
+}
+
+/// Immutable resolved bootstrap identity and canonical paths.
+///
+/// External callers cannot forge or mutate a context:
+///
+/// ```compile_fail
+/// use radroots_runtime_paths::RuntimeContext;
+///
+/// let _ = RuntimeContext {
+/// service: todo!(),
+/// instance: todo!(),
+/// profile: todo!(),
+/// paths: todo!(),
+/// sources: todo!(),
+/// };
+/// ```
+#[derive(Clone, PartialEq, Eq)]
+pub struct RuntimeContext {
+ service: ServiceId,
+ instance: InstanceId,
+ profile: RadrootsPathProfile,
+ paths: RadrootsServiceInstancePaths,
+ sources: RuntimeContextSources,
+}
+
+impl RuntimeContext {
+ pub fn resolve(
+ resolver: &RadrootsPathResolver,
+ bootstrap: RuntimeContextBootstrap,
+ service: ServiceId,
+ instance: InstanceId,
+ ) -> Result<Self, RuntimeContextError> {
+ let overrides = match bootstrap.repo_local_root.as_ref() {
+ Some(root) => RadrootsPathOverrides::repo_local(root),
+ None => RadrootsPathOverrides::default(),
+ };
+ let paths = default_service_instance_paths(
+ resolver,
+ bootstrap.profile,
+ &overrides,
+ &service,
+ &instance,
+ )
+ .map_err(|_| RuntimeContextError::PathSelection)?;
+ let sources = RuntimeContextSources {
+ service: RuntimeContextSource::SafeDefault,
+ instance: bootstrap.instance_source,
+ profile: bootstrap.profile_source,
+ repo_local_root: bootstrap
+ .repo_local_root
+ .as_ref()
+ .map(|_| RuntimeContextSource::BootstrapCli),
+ paths: RuntimeContextSource::DerivedPath,
+ };
+
+ Ok(Self {
+ service,
+ instance,
+ profile: bootstrap.profile,
+ paths,
+ sources,
+ })
+ }
+
+ #[must_use]
+ pub fn service(&self) -> &ServiceId {
+ &self.service
+ }
+
+ #[must_use]
+ pub fn instance(&self) -> &InstanceId {
+ &self.instance
+ }
+
+ #[must_use]
+ pub fn profile(&self) -> RadrootsPathProfile {
+ self.profile
+ }
+
+ #[must_use]
+ pub fn paths(&self) -> &RadrootsServiceInstancePaths {
+ &self.paths
+ }
+
+ #[must_use]
+ pub fn sources(&self) -> &RuntimeContextSources {
+ &self.sources
+ }
+}
+
+impl fmt::Debug for RuntimeContext {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RuntimeContext")
+ .field("service", &self.service)
+ .field("instance", &self.instance)
+ .field("profile", &self.profile)
+ .field("paths", &"[redacted]")
+ .field("sources", &self.sources)
+ .finish()
+ }
+}
+
+impl Serialize for RuntimeContext {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut state = serializer.serialize_struct("RuntimeContext", 5)?;
+ state.serialize_field("service", &self.service)?;
+ state.serialize_field("instance", &self.instance)?;
+ state.serialize_field("profile", &self.profile.to_string())?;
+ state.serialize_field("paths", "[redacted]")?;
+ state.serialize_field("sources", &self.sources)?;
+ state.end()
+ }
+}
+
+/// Safe construction failures for [`RuntimeContext`].
+#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
+pub enum RuntimeContextError {
+ #[error("runtime context bootstrap provenance does not match its selectors")]
+ InvalidBootstrapBinding,
+ #[error("runtime context path selection failed")]
+ PathSelection,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{path::PathBuf, time::SystemTime};
+
+ use serde_json::json;
+
+ use super::{
+ RuntimeContext, RuntimeContextBootstrap, RuntimeContextError, RuntimeContextSource,
+ };
+ use crate::{
+ InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform, ServiceId,
+ };
+
+ fn repo_local_context(base: PathBuf) -> RuntimeContext {
+ RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(base),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new("myc").expect("service"),
+ InstanceId::new("primary").expect("instance"),
+ )
+ .expect("runtime context")
+ }
+
+ #[test]
+ fn context_is_equal_immutable_and_preserves_exact_typed_sources() {
+ let first = repo_local_context(PathBuf::from("/repo/.local/radroots"));
+ let second = repo_local_context(PathBuf::from("/repo/.local/radroots"));
+ assert_eq!(first, second);
+ assert_eq!(first.service().as_str(), "myc");
+ assert_eq!(first.instance().as_str(), "primary");
+ assert_eq!(first.profile(), RadrootsPathProfile::RepoLocal);
+ assert_eq!(first.sources().service(), RuntimeContextSource::SafeDefault);
+ assert_eq!(
+ first.sources().instance(),
+ RuntimeContextSource::BootstrapCli
+ );
+ assert_eq!(
+ first.sources().profile(),
+ RuntimeContextSource::BootstrapCli
+ );
+ assert_eq!(
+ first.sources().repo_local_root(),
+ Some(RuntimeContextSource::BootstrapCli)
+ );
+ assert_eq!(first.sources().paths(), RuntimeContextSource::DerivedPath);
+ assert_eq!(
+ first.paths().config(),
+ PathBuf::from("/repo/.local/radroots/config/services/myc/primary")
+ );
+
+ let defaulted = RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::ServiceHost,
+ None,
+ RuntimeContextSource::SafeDefault,
+ RuntimeContextSource::SafeDefault,
+ )
+ .expect("default bootstrap"),
+ ServiceId::new("rhi").expect("service"),
+ InstanceId::new("default").expect("instance"),
+ )
+ .expect("default runtime context");
+ assert_eq!(
+ defaulted.sources().profile(),
+ RuntimeContextSource::SafeDefault
+ );
+ assert_eq!(
+ defaulted.sources().instance(),
+ RuntimeContextSource::SafeDefault
+ );
+ assert_eq!(defaulted.sources().repo_local_root(), None);
+ }
+
+ #[test]
+ fn serialization_and_debug_redact_paths_and_use_only_closed_sources() {
+ let bootstrap = RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from("/sensitive/project-root")),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap");
+ let bootstrap_debug = format!("{bootstrap:?}");
+ assert!(bootstrap_debug.contains("repo_local_root: Some(\"[redacted]\")"));
+ let context = repo_local_context(PathBuf::from("/sensitive/project-root"));
+ let serialized = serde_json::to_value(&context).expect("serialize context");
+ assert_eq!(
+ serialized,
+ json!({
+ "service": "myc",
+ "instance": "primary",
+ "profile": "repo_local",
+ "paths": "[redacted]",
+ "sources": {
+ "service": "safe_default",
+ "instance": "bootstrap_cli",
+ "profile": "bootstrap_cli",
+ "repo_local_root": "bootstrap_cli",
+ "paths": "derived_path"
+ }
+ })
+ );
+ assert_eq!(
+ serde_json::to_value([
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::Toml,
+ RuntimeContextSource::SafeDefault,
+ RuntimeContextSource::DerivedPath,
+ ])
+ .expect("source inventory"),
+ json!(["bootstrap_cli", "toml", "safe_default", "derived_path"])
+ );
+ let debug = format!("{context:?}");
+ assert!(debug.contains("paths: \"[redacted]\""));
+ for forbidden in [
+ "/sensitive",
+ "project-root",
+ "/config/",
+ "/run/",
+ "secret:caller-controlled-value",
+ "0123456789abcdef0123456789abcdef",
+ ] {
+ assert!(!serialized.to_string().contains(forbidden));
+ assert!(!debug.contains(forbidden));
+ assert!(!bootstrap_debug.contains(forbidden));
+ }
+ }
+
+ #[test]
+ fn construction_performs_no_directory_file_or_ambient_bootstrap_io() {
+ let unique = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .expect("clock after epoch")
+ .as_nanos();
+ let base = std::env::temp_dir().join(format!(
+ "radroots-runtime-context-no-io-{}-{unique}",
+ std::process::id()
+ ));
+ assert!(!base.exists(), "unique test base unexpectedly exists");
+ let context = repo_local_context(base.clone());
+ assert_eq!(
+ context.paths().state(),
+ base.join("data/services/myc/primary")
+ );
+ assert!(!base.exists(), "context construction created the base");
+
+ let production = include_str!("context.rs")
+ .split("#[cfg(test)]")
+ .next()
+ .expect("production source");
+ for forbidden in [
+ "std::fs",
+ "create_dir",
+ "create_file",
+ "OpenOptions",
+ "std::env",
+ ] {
+ assert!(
+ !production.contains(forbidden),
+ "context production source contains forbidden I/O `{forbidden}`"
+ );
+ }
+ }
+
+ #[test]
+ fn typed_bootstrap_rejects_every_mismatched_provenance_combination() {
+ for source in [
+ RuntimeContextSource::Toml,
+ RuntimeContextSource::DerivedPath,
+ ] {
+ assert_eq!(
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::ServiceHost,
+ None,
+ source,
+ RuntimeContextSource::BootstrapCli,
+ ),
+ Err(RuntimeContextError::InvalidBootstrapBinding)
+ );
+ assert_eq!(
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::ServiceHost,
+ None,
+ RuntimeContextSource::SafeDefault,
+ source,
+ ),
+ Err(RuntimeContextError::InvalidBootstrapBinding)
+ );
+ }
+ assert_eq!(
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ None,
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ ),
+ Err(RuntimeContextError::InvalidBootstrapBinding)
+ );
+ assert_eq!(
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from("/repo/.local/radroots")),
+ RuntimeContextSource::SafeDefault,
+ RuntimeContextSource::SafeDefault,
+ ),
+ Err(RuntimeContextError::InvalidBootstrapBinding)
+ );
+ assert_eq!(
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::ServiceHost,
+ Some(PathBuf::from("/repo/.local/radroots")),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ ),
+ Err(RuntimeContextError::InvalidBootstrapBinding)
+ );
+ assert_eq!(
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::MobileNative,
+ None,
+ RuntimeContextSource::SafeDefault,
+ RuntimeContextSource::SafeDefault,
+ ),
+ Err(RuntimeContextError::InvalidBootstrapBinding)
+ );
+ }
+}
diff --git a/crates/runtime_paths/src/lib.rs b/crates/runtime_paths/src/lib.rs
@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
+pub mod context;
pub mod conventions;
pub mod error;
pub mod identifier;
@@ -8,6 +9,10 @@ pub mod platform;
pub mod roots;
pub mod service;
+pub use context::{
+ RuntimeContext, RuntimeContextBootstrap, RuntimeContextError, RuntimeContextSource,
+ RuntimeContextSources,
+};
pub use conventions::{
DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME,
DEFAULT_SHARED_GEONAMES_NAMESPACE, DEFAULT_SHARED_GEONAMES_NAMESPACE_KIND,