lib

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

commit 6d61cfc2b4fe9c3b1914c2d6a8077b52b29e02e1
parent 21b11e7a5120ea949f7ad0838c746873fc73aac2
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 23:58:11 +0000

runtime-manager: bind typed service control

- make RuntimeContext the sole managed service-instance authority
- add sealed CLI-v1 plans and bounded Unix status-v1 access
- remove prototype registry, path, PID, and endpoint assumptions
- freeze package boundaries, portable checks, and the updated API baseline

Diffstat:
MCargo.lock | 4++++
Mcontracts/api_baselines/radroots_runtime_paths.txt | 1+
Mcrates/runtime_manager/Cargo.toml | 10+++++++++-
Mcrates/runtime_manager/README | 29++++++++++++++++++++---------
Acrates/runtime_manager/src/cli.rs | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/runtime_manager/src/error.rs | 37+++++++++++++++++++++++++++++++++++++
Mcrates/runtime_manager/src/lib.rs | 20++++++++++++++------
Mcrates/runtime_manager/src/managed.rs | 232++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mcrates/runtime_manager/src/model.rs | 79++-----------------------------------------------------------------------------
Acrates/runtime_manager/src/status.rs | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/runtime_manager/tests/fixtures/hardened_service_management.v1.toml | 35+++++++----------------------------
Mcrates/runtime_manager/tests/service_target_boundary.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcrates/runtime_paths/src/context.rs | 34+++++++++++++++++++++++++++-------
Mcrates/runtime_paths/tests/package_boundary.rs | 1+
14 files changed, 786 insertions(+), 252 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3407,8 +3407,12 @@ version = "0.1.0-alpha" dependencies = [ "radroots_runtime_distribution", "radroots_runtime_paths", + "radroots_service_host", "serde", + "serde_json", + "tempfile", "thiserror 1.0.69", + "tokio", "toml 0.8.23", ] diff --git a/contracts/api_baselines/radroots_runtime_paths.txt b/contracts/api_baselines/radroots_runtime_paths.txt @@ -116,6 +116,7 @@ impl radroots_runtime_paths::RuntimeContext pub fn radroots_runtime_paths::RuntimeContext::instance(&self) -> &radroots_runtime_paths::InstanceId pub fn radroots_runtime_paths::RuntimeContext::paths(&self) -> &radroots_runtime_paths::RadrootsServiceInstancePaths pub fn radroots_runtime_paths::RuntimeContext::profile(&self) -> radroots_runtime_paths::RadrootsPathProfile +pub fn radroots_runtime_paths::RuntimeContext::repo_local_root(&self) -> core::option::Option<&std::path::Path> pub fn radroots_runtime_paths::RuntimeContext::resolve(&radroots_runtime_paths::RadrootsPathResolver, radroots_runtime_paths::RuntimeContextBootstrap, radroots_runtime_paths::ServiceId, radroots_runtime_paths::InstanceId) -> core::result::Result<Self, radroots_runtime_paths::RuntimeContextError> pub fn radroots_runtime_paths::RuntimeContext::service(&self) -> &radroots_runtime_paths::ServiceId pub fn radroots_runtime_paths::RuntimeContext::sources(&self) -> &radroots_runtime_paths::RuntimeContextSources diff --git a/crates/runtime_manager/Cargo.toml b/crates/runtime_manager/Cargo.toml @@ -6,7 +6,7 @@ edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true license.workspace = true -description = "Metadata-only hardened service target resolver for Radroots" +description = "Typed hardened-service runtime management boundary for Radroots" repository.workspace = true homepage.workspace = true documentation = "https://docs.rs/radroots_runtime_manager" @@ -18,3 +18,11 @@ radroots_runtime_paths = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } toml = { workspace = true } + +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +radroots_service_host = { workspace = true } + +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dev-dependencies] +serde_json = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["io-util", "macros", "net", "rt"] } diff --git a/crates/runtime_manager/README b/crates/runtime_manager/README @@ -1,19 +1,30 @@ # radroots_runtime_manager `radroots_runtime_manager` validates the frozen runtime-management contract and -resolves explicitly selected Myc and RHI service-instance identities to sealed, -metadata-only targets. +binds explicitly resolved Myc and RHI `RuntimeContext` values to the common +service-management interfaces. -The crate performs no filesystem, registry, process, archive, artifact, -configuration, log, PID, install, removal, or lifecycle work. It exposes no -runtime paths or raw persistence helpers. Final service lifecycle and artifact -behavior remains unavailable until the separately governed Steps 219 and 220. +The sealed target uses its `RuntimeContext` as the sole service, instance, +profile, and canonical-path authority. It can produce exact CLI-v1 argument +plans for the common `config init`, `config validate`, `state init`, `run`, +`status`, and `doctor` commands. On Linux and macOS it can also construct the +shared bounded HTTP/1.1-over-Unix admin client for the fixed `/v1/status` +endpoint. Service-owned typed status models must expose the common v1 identity +projection, and every response is rejected unless its contract version, +service, and instance match the selected runtime context. + +This crate never discovers or executes a program, opens a PID/config/log file, +reads credentials, owns a service process, mutates canonical paths, or selects +an archive, binary, channel, package, or install location. Artifact and +distribution resolution remains separately governed by Step220. The status +client performs only the bounded request explicitly initiated by its caller; +target construction itself performs no I/O. Complete management TOML documents are rejected before parsing when they exceed exactly 1,048,576 UTF-8 bytes. Bounded documents still require the exact -schema, version, closed field set, empty lifecycle inventory, and complete -Myc/RHI metadata inventory. Directly constructed contracts are revalidated -before a management context can be created. +schema, version, closed field set, active Myc/RHI inventory, common CLI/admin +clients, lifecycle vocabulary, and service metadata. Directly constructed +contracts are revalidated before a management context can be created. ## Copyright diff --git a/crates/runtime_manager/src/cli.rs b/crates/runtime_manager/src/cli.rs @@ -0,0 +1,255 @@ +//! Sealed CLI-v1 argument plans for hardened service management. + +use core::fmt; +use std::ffi::{OsStr, OsString}; + +use radroots_runtime_paths::{RadrootsPathProfile, RuntimeContext}; + +use crate::RadrootsRuntimeManagerError; + +const CLI_PATH_MAX_UTF8_BYTES: usize = 4_096; + +/// Common hardened-service CLI-v1 operations governed by this package. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ManagedCliCommand { + ConfigInit, + ConfigValidate, + StateInit, + Run, + Status, + Doctor, +} + +impl ManagedCliCommand { + fn tokens(self) -> &'static [&'static str] { + match self { + Self::ConfigInit => &["config", "init"], + Self::ConfigValidate => &["config", "validate"], + Self::StateInit => &["state", "init"], + Self::Run => &["run"], + Self::Status => &["status"], + Self::Doctor => &["doctor"], + } + } +} + +/// Validated arguments for a caller-owned Myc or RHI executable. +/// +/// The plan intentionally contains no program, executable, archive, channel, +/// or install path. Those distribution concerns remain outside Step219. +/// External construction is sealed so every argument remains bound to the +/// selected [`RuntimeContext`]. +/// +/// ```compile_fail +/// use radroots_runtime_manager::ManagedCliInvocation; +/// +/// let _ = ManagedCliInvocation { +/// command: todo!(), +/// profile: todo!(), +/// arguments: todo!(), +/// }; +/// ``` +#[derive(Clone, PartialEq, Eq)] +pub struct ManagedCliInvocation { + command: ManagedCliCommand, + profile: RadrootsPathProfile, + arguments: Box<[OsString]>, +} + +impl ManagedCliInvocation { + pub(crate) fn for_context( + context: &RuntimeContext, + command: ManagedCliCommand, + ) -> Result<Self, RadrootsRuntimeManagerError> { + let profile = cli_profile(context.profile())?; + let mut arguments = Vec::with_capacity(9); + arguments.extend([ + OsString::from("--profile"), + OsString::from(profile), + OsString::from("--instance"), + OsString::from(context.instance().as_str()), + ]); + if context.profile() == RadrootsPathProfile::RepoLocal { + let root = context + .repo_local_root() + .ok_or(RadrootsRuntimeManagerError::ContextMismatch)?; + if root + .to_str() + .is_none_or(|value| value.len() > CLI_PATH_MAX_UTF8_BYTES) + { + return Err(RadrootsRuntimeManagerError::ContextMismatch); + } + arguments.push(OsString::from("--repo-local-root")); + arguments.push(root.as_os_str().to_owned()); + } else if context.repo_local_root().is_some() { + return Err(RadrootsRuntimeManagerError::ContextMismatch); + } + arguments.extend(command.tokens().iter().map(OsString::from)); + Ok(Self { + command, + profile: context.profile(), + arguments: arguments.into_boxed_slice(), + }) + } + + #[must_use] + pub const fn command(&self) -> ManagedCliCommand { + self.command + } + + #[must_use] + pub const fn profile(&self) -> RadrootsPathProfile { + self.profile + } + + #[must_use] + pub fn arguments(&self) -> &[OsString] { + &self.arguments + } +} + +impl fmt::Debug for ManagedCliInvocation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ManagedCliInvocation") + .field("command", &self.command) + .field("profile", &self.profile) + .field("argument_count", &self.arguments.len()) + .field("arguments", &"[redacted]") + .finish() + } +} + +fn cli_profile( + profile: RadrootsPathProfile, +) -> Result<&'static OsStr, RadrootsRuntimeManagerError> { + match profile { + RadrootsPathProfile::InteractiveUser => Ok(OsStr::new("interactive")), + RadrootsPathProfile::ServiceHost => Ok(OsStr::new("service-host")), + RadrootsPathProfile::RepoLocal => Ok(OsStr::new("repo-local")), + RadrootsPathProfile::MobileNative => Err(RadrootsRuntimeManagerError::UnsupportedProfile), + } +} + +#[cfg(test)] +mod tests { + use std::{ffi::OsString, path::PathBuf}; + + use radroots_runtime_paths::{ + InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, + }; + + use super::{ManagedCliCommand, ManagedCliInvocation, cli_profile}; + + fn context(profile: RadrootsPathProfile) -> RuntimeContext { + let root = (profile == RadrootsPathProfile::RepoLocal) + .then(|| PathBuf::from("/sensitive/project-root")); + RuntimeContext::resolve( + &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), + RuntimeContextBootstrap::new( + profile, + root, + if profile == RadrootsPathProfile::RepoLocal { + RuntimeContextSource::BootstrapCli + } else { + RuntimeContextSource::SafeDefault + }, + RuntimeContextSource::BootstrapCli, + ) + .expect("bootstrap"), + ServiceId::new("myc").expect("service"), + InstanceId::new("primary").expect("instance"), + ) + .expect("context") + } + + #[test] + fn every_common_command_uses_the_exact_cli_v1_shape() { + for (command, suffix) in [ + (ManagedCliCommand::ConfigInit, &["config", "init"][..]), + ( + ManagedCliCommand::ConfigValidate, + &["config", "validate"][..], + ), + (ManagedCliCommand::StateInit, &["state", "init"][..]), + (ManagedCliCommand::Run, &["run"][..]), + (ManagedCliCommand::Status, &["status"][..]), + (ManagedCliCommand::Doctor, &["doctor"][..]), + ] { + let invocation = ManagedCliInvocation::for_context( + &context(RadrootsPathProfile::ServiceHost), + command, + ) + .expect("invocation"); + let mut expected = vec![ + OsString::from("--profile"), + OsString::from("service-host"), + OsString::from("--instance"), + OsString::from("primary"), + ]; + expected.extend(suffix.iter().map(OsString::from)); + assert_eq!(invocation.arguments(), expected); + assert_eq!(invocation.command(), command); + } + } + + #[test] + fn profile_names_match_both_service_cli_v1_parsers() { + for (profile, expected) in [ + (RadrootsPathProfile::InteractiveUser, "interactive"), + (RadrootsPathProfile::ServiceHost, "service-host"), + (RadrootsPathProfile::RepoLocal, "repo-local"), + ] { + assert_eq!(cli_profile(profile).expect("profile"), expected); + } + assert!(cli_profile(RadrootsPathProfile::MobileNative).is_err()); + } + + #[test] + fn repo_local_plan_preserves_the_validated_explicit_root_and_redacts_debug() { + let invocation = ManagedCliInvocation::for_context( + &context(RadrootsPathProfile::RepoLocal), + ManagedCliCommand::Run, + ) + .expect("invocation"); + assert_eq!( + invocation.arguments(), + [ + "--profile", + "repo-local", + "--instance", + "primary", + "--repo-local-root", + "/sensitive/project-root", + "run", + ] + .map(OsString::from) + ); + let debug = format!("{invocation:?}"); + assert!(!debug.contains("sensitive")); + assert!(!debug.contains("project-root")); + } + + #[test] + fn cli_plan_rejects_a_context_root_outside_the_cli_v1_text_bound() { + let root = format!("/{}", "x".repeat(super::CLI_PATH_MAX_UTF8_BYTES)); + let context = RuntimeContext::resolve( + &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), + RuntimeContextBootstrap::new( + RadrootsPathProfile::RepoLocal, + Some(PathBuf::from(root)), + RuntimeContextSource::BootstrapCli, + RuntimeContextSource::BootstrapCli, + ) + .expect("bootstrap"), + ServiceId::new("myc").expect("service"), + InstanceId::new("primary").expect("instance"), + ) + .expect("context"); + assert_eq!( + ManagedCliInvocation::for_context(&context, ManagedCliCommand::Run), + Err(crate::RadrootsRuntimeManagerError::ContextMismatch) + ); + } +} diff --git a/crates/runtime_manager/src/error.rs b/crates/runtime_manager/src/error.rs @@ -17,4 +17,41 @@ pub enum RadrootsRuntimeManagerError { UnsupportedProfile, #[error("runtime is not a hardened service target")] UnsupportedServiceTarget, + #[error("runtime context does not match the selected management target")] + ContextMismatch, + #[error("bounded local admin client construction failed")] + AdminClient, + #[error("bounded local admin request failed")] + AdminRequest, + #[error("service status response does not match the selected runtime context")] + StatusContractMismatch, +} + +#[cfg(test)] +mod tests { + use std::error::Error as _; + + use super::RadrootsRuntimeManagerError; + + #[test] + fn every_public_failure_is_fixed_value_free_and_source_free() { + for error in [ + RadrootsRuntimeManagerError::ContractTooLarge, + RadrootsRuntimeManagerError::Parse, + RadrootsRuntimeManagerError::UnexpectedSchema, + RadrootsRuntimeManagerError::UnexpectedSchemaVersion, + RadrootsRuntimeManagerError::InvalidContract, + RadrootsRuntimeManagerError::UnsupportedProfile, + RadrootsRuntimeManagerError::UnsupportedServiceTarget, + RadrootsRuntimeManagerError::ContextMismatch, + RadrootsRuntimeManagerError::AdminClient, + RadrootsRuntimeManagerError::AdminRequest, + RadrootsRuntimeManagerError::StatusContractMismatch, + ] { + let rendered = format!("{error} {error:?}"); + assert!(!rendered.contains('/')); + assert!(!rendered.contains("secret-value")); + assert!(error.source().is_none()); + } + } } diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs @@ -1,16 +1,21 @@ #![forbid(unsafe_code)] +mod cli; mod error; mod managed; mod model; +#[cfg(any(target_os = "linux", target_os = "macos"))] +mod status; +pub use cli::{ManagedCliCommand, ManagedCliInvocation}; pub use error::RadrootsRuntimeManagerError; pub use managed::{ManagedRuntimeContext, ManagedRuntimeTarget, resolve_runtime_target}; pub use model::{ - BootstrapRuntimeContract, InstanceMetadataContract, LifecycleContract, ManagementDefaults, - ManagementModeContract, ManagementPathContract, RadrootsRuntimeManagementContract, - RuntimeGroups, + InstanceMetadataContract, LifecycleContract, ManagementDefaults, ManagementModeContract, + RadrootsRuntimeManagementContract, RuntimeGroups, }; +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub use status::{ManagedRuntimeStatusClient, ManagedServiceStatusV1}; pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management"; pub const RUNTIME_MANAGEMENT_SCHEMA_VERSION: u32 = 1; @@ -65,15 +70,18 @@ mod tests { let contract = parse_contract_str(CONTRACT).expect("contract"); assert_eq!(contract.schema, RUNTIME_MANAGEMENT_SCHEMA); assert_eq!(contract.service_targets.len(), 2); - assert!(contract.bootstrap.is_empty()); + assert_eq!(contract.managed_runtime_targets.active, ["myc", "rhi"]); for raw in [ CONTRACT.replace("schema_version = 1", "schema_version = 2"), CONTRACT.replace( - "defined = [\"myc\", \"rhi\"]", + "active = [\"myc\", \"rhi\"]", "active = [\"myc\"]\ndefined = [\"rhi\"]", ), - CONTRACT.replace("actions = []", "actions = [\"start\"]"), + CONTRACT.replace( + "actions = [\"config_init\", \"config_validate\", \"state_init\", \"run\", \"status\", \"doctor\"]", + "actions = [\"start\"]", + ), format!("{CONTRACT}\nunknown = true\n"), ] { assert!(parse_contract_str(&raw).is_err()); diff --git a/crates/runtime_manager/src/managed.rs b/crates/runtime_manager/src/managed.rs @@ -1,86 +1,68 @@ use core::fmt; use radroots_runtime_distribution::HardenedServiceTarget; -use radroots_runtime_paths::{InstanceId, RadrootsPathProfile, ServiceId}; +use radroots_runtime_paths::{InstanceId, RadrootsPathProfile, RuntimeContext, ServiceId}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use radroots_service_host::AdminTransportLimits; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use crate::ManagedRuntimeStatusClient; use crate::{ - ManagementModeContract, RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError, + ManagedCliCommand, ManagedCliInvocation, ManagementModeContract, + RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError, }; -/// Validated metadata-only runtime-management context. +/// Validated frozen runtime-management contract. /// -/// The context owns no filesystem path, registry, process, artifact, or -/// lifecycle capability. Its fields are private so a caller cannot bypass the -/// exact contract validation performed by [`ManagedRuntimeContext::new`]. +/// Instance identity, profile, and canonical paths are deliberately absent; +/// those values enter only through a sealed [`RuntimeContext`] when a target is +/// resolved. /// /// ```compile_fail /// use radroots_runtime_manager::ManagedRuntimeContext; /// -/// let _ = ManagedRuntimeContext { -/// contract: todo!(), -/// profile: todo!(), -/// management_mode: String::new(), -/// }; +/// let _ = ManagedRuntimeContext { contract: todo!() }; /// ``` #[derive(Clone)] pub struct ManagedRuntimeContext { contract: RadrootsRuntimeManagementContract, - profile: RadrootsPathProfile, - management_mode: String, } impl ManagedRuntimeContext { pub fn new( contract: RadrootsRuntimeManagementContract, - profile: RadrootsPathProfile, ) -> Result<Self, RadrootsRuntimeManagerError> { crate::validate_hardened_management_contract(&contract)?; - let management_mode = active_management_mode_for_profile(&contract, profile)?.to_owned(); - Ok(Self { - contract, - profile, - management_mode, - }) + Ok(Self { contract }) } #[must_use] pub fn contract(&self) -> &RadrootsRuntimeManagementContract { &self.contract } - - #[must_use] - pub const fn profile(&self) -> RadrootsPathProfile { - self.profile - } - - #[must_use] - pub fn management_mode(&self) -> &str { - &self.management_mode - } } impl fmt::Debug for ManagedRuntimeContext { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("ManagedRuntimeContext") - .field("profile", &self.profile) - .field("management_mode", &self.management_mode) + .field("schema", &self.contract.schema) + .field("schema_version", &self.contract.schema_version) .finish_non_exhaustive() } } -/// Sealed metadata for one explicitly selected Myc or RHI instance. +/// Sealed management capability for one validated Myc or RHI runtime context. /// -/// The target deliberately carries no resolved runtime paths and exposes no -/// lifecycle, registry, filesystem, process, or artifact capability. +/// The target owns the sole service-instance identity/profile/path authority. +/// It exposes typed CLI-v1 and status-v1 integration but no filesystem, +/// process, PID, log, credential, or distribution-artifact mutation surface. /// /// ```compile_fail /// use radroots_runtime_manager::ManagedRuntimeTarget; /// /// let _ = ManagedRuntimeTarget { -/// service_id: todo!(), -/// instance_id: todo!(), -/// profile: todo!(), +/// context: todo!(), /// service_target: todo!(), /// management_mode: String::new(), /// mode_contract: todo!(), @@ -88,9 +70,7 @@ impl fmt::Debug for ManagedRuntimeContext { /// ``` #[derive(Clone)] pub struct ManagedRuntimeTarget { - service_id: ServiceId, - instance_id: InstanceId, - profile: RadrootsPathProfile, + context: RuntimeContext, service_target: HardenedServiceTarget, management_mode: String, mode_contract: ManagementModeContract, @@ -98,18 +78,23 @@ pub struct ManagedRuntimeTarget { impl ManagedRuntimeTarget { #[must_use] + pub fn runtime_context(&self) -> &RuntimeContext { + &self.context + } + + #[must_use] pub fn service_id(&self) -> &ServiceId { - &self.service_id + self.context.service() } #[must_use] pub fn instance_id(&self) -> &InstanceId { - &self.instance_id + self.context.instance() } #[must_use] - pub const fn profile(&self) -> RadrootsPathProfile { - self.profile + pub fn profile(&self) -> RadrootsPathProfile { + self.context.profile() } #[must_use] @@ -126,44 +111,62 @@ impl ManagedRuntimeTarget { pub fn mode_contract(&self) -> &ManagementModeContract { &self.mode_contract } + + pub fn cli_invocation( + &self, + command: ManagedCliCommand, + ) -> Result<ManagedCliInvocation, RadrootsRuntimeManagerError> { + ManagedCliInvocation::for_context(&self.context, command) + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + pub fn status_client( + &self, + limits: AdminTransportLimits, + ) -> Result<ManagedRuntimeStatusClient, RadrootsRuntimeManagerError> { + ManagedRuntimeStatusClient::for_context(&self.context, limits) + } } impl fmt::Debug for ManagedRuntimeTarget { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("ManagedRuntimeTarget") - .field("service_id", &self.service_id) - .field("instance_id", &self.instance_id) - .field("profile", &self.profile) + .field("service_id", self.context.service()) + .field("instance_id", self.context.instance()) + .field("profile", &self.context.profile()) .field("management_mode", &self.management_mode) + .field("paths", &"[redacted]") .finish_non_exhaustive() } } pub fn resolve_runtime_target( context: &ManagedRuntimeContext, - service_id: ServiceId, - instance_id: InstanceId, + runtime_context: RuntimeContext, ) -> Result<ManagedRuntimeTarget, RadrootsRuntimeManagerError> { let service_target = context .contract .service_targets - .get(&service_id) + .get(runtime_context.service()) .cloned() .ok_or(RadrootsRuntimeManagerError::UnsupportedServiceTarget)?; + let management_mode = + active_management_mode_for_profile(&context.contract, runtime_context.profile())?; let mode_contract = context .contract .mode - .get(&context.management_mode) + .get(management_mode) .cloned() .ok_or(RadrootsRuntimeManagerError::InvalidContract)?; + if service_target.service_id() != runtime_context.service() { + return Err(RadrootsRuntimeManagerError::ContextMismatch); + } Ok(ManagedRuntimeTarget { - service_id, - instance_id, - profile: context.profile, + context: runtime_context, service_target, - management_mode: context.management_mode.clone(), + management_mode: management_mode.to_owned(), mode_contract, }) } @@ -189,68 +192,75 @@ fn active_management_mode_for_profile( #[cfg(test)] mod tests { - use radroots_runtime_paths::{InstanceId, RadrootsPathProfile, ServiceId}; + use std::path::PathBuf; + + use radroots_runtime_paths::{ + InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, + }; use super::{ManagedRuntimeContext, resolve_runtime_target}; use crate::{HARDENED_MANAGEMENT_CONTRACT, RadrootsRuntimeManagerError, parse_contract_str}; - fn context(profile: RadrootsPathProfile) -> ManagedRuntimeContext { + fn management_context() -> ManagedRuntimeContext { ManagedRuntimeContext::new( parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"), - profile, ) .expect("management context") } - fn target( - context: &ManagedRuntimeContext, + fn runtime_context( + profile: RadrootsPathProfile, service: &str, instance: &str, - ) -> super::ManagedRuntimeTarget { - resolve_runtime_target( - context, + ) -> RuntimeContext { + let root = (profile == RadrootsPathProfile::RepoLocal) + .then(|| PathBuf::from("/sensitive/project-root")); + RuntimeContext::resolve( + &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), + RuntimeContextBootstrap::new( + profile, + root, + if profile == RadrootsPathProfile::RepoLocal { + RuntimeContextSource::BootstrapCli + } else { + RuntimeContextSource::SafeDefault + }, + RuntimeContextSource::BootstrapCli, + ) + .expect("bootstrap"), ServiceId::new(service).expect("service"), InstanceId::new(instance).expect("instance"), ) - .expect("target") + .expect("runtime context") } #[test] - fn contexts_accept_only_exact_contracts_and_supported_profiles() { - for (profile, mode) in [ - (RadrootsPathProfile::RepoLocal, "interactive_user_managed"), - (RadrootsPathProfile::ServiceHost, "service_host_managed"), - ] { - let context = context(profile); - assert_eq!(context.profile(), profile); - assert_eq!(context.management_mode(), mode); - assert_eq!(context.contract().service_targets.len(), 2); - } - - for profile in [ - RadrootsPathProfile::InteractiveUser, - RadrootsPathProfile::MobileNative, - ] { - let contract = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); - assert!(matches!( - ManagedRuntimeContext::new(contract, profile), - Err(RadrootsRuntimeManagerError::UnsupportedProfile) - )); - } + fn context_accepts_only_the_exact_contract() { + let context = management_context(); + assert_eq!(context.contract().service_targets.len(), 2); let mut direct = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); direct.lifecycle.actions.push("start".to_owned()); assert!(matches!( - ManagedRuntimeContext::new(direct, RadrootsPathProfile::RepoLocal), + ManagedRuntimeContext::new(direct), Err(RadrootsRuntimeManagerError::InvalidContract) )); } #[test] - fn exact_myc_and_rhi_instances_resolve_to_static_metadata_only() { - let context = context(RadrootsPathProfile::RepoLocal); - let myc = target(&context, "myc", "primary"); - let rhi = target(&context, "rhi", "secondary"); + fn exact_myc_and_rhi_contexts_resolve_without_identity_duplication() { + let management = management_context(); + let myc = resolve_runtime_target( + &management, + runtime_context(RadrootsPathProfile::RepoLocal, "myc", "primary"), + ) + .expect("myc target"); + let rhi = resolve_runtime_target( + &management, + runtime_context(RadrootsPathProfile::ServiceHost, "rhi", "secondary"), + ) + .expect("rhi target"); assert_eq!(myc.service_id().as_str(), "myc"); assert_eq!(myc.instance_id().as_str(), "primary"); @@ -261,33 +271,41 @@ mod tests { assert_eq!(rhi.service_id().as_str(), "rhi"); assert_eq!(rhi.instance_id().as_str(), "secondary"); - assert_eq!(rhi.service_target().service_id(), rhi.service_id()); + assert_eq!(rhi.management_mode(), "service_host_managed"); + assert!(rhi.mode_contract().service_manager_integration); + assert_eq!(rhi.runtime_context().service(), rhi.service_id()); for rendered in [ - format!("{context:?}"), + format!("{management:?}"), format!("{myc:?}"), format!("{rhi:?}"), ] { - assert!(!rendered.contains('/')); + assert!(!rendered.contains("sensitive")); assert!(!rendered.contains("state.sqlite")); - assert!(!rendered.contains("instances.toml")); + assert!(!rendered.contains("admin.sock")); } } #[test] - fn unsupported_service_ids_fail_without_fallback_or_effects() { - let context = context(RadrootsPathProfile::ServiceHost); - let error = resolve_runtime_target( - &context, - ServiceId::new("radrootsd").expect("service"), - InstanceId::new("default").expect("instance"), - ) - .expect_err("unsupported target"); - assert_eq!(error, RadrootsRuntimeManagerError::UnsupportedServiceTarget); + fn unsupported_service_and_profile_fail_without_fallback() { + let management = management_context(); + let unsupported = runtime_context(RadrootsPathProfile::ServiceHost, "radrootsd", "default"); + assert!(matches!( + resolve_runtime_target(&management, unsupported), + Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget) + )); + + let mobile = RuntimeContextBootstrap::new( + RadrootsPathProfile::MobileNative, + None, + RuntimeContextSource::SafeDefault, + RuntimeContextSource::BootstrapCli, + ); + assert!(mobile.is_err()); } #[test] - fn production_manager_is_metadata_only_and_contains_no_io_authority() { + fn production_manager_has_no_direct_io_process_or_artifact_authority() { let source = include_str!("managed.rs") .split("\n#[cfg(test)]") .next() @@ -295,8 +313,6 @@ mod tests { for forbidden in [ "std::fs", "std::process", - "std::path", - "radroots_runtime_paths::RuntimeContext", "load_registry", "save_registry", "register_instance", @@ -310,7 +326,7 @@ mod tests { ] { assert!( !source.contains(forbidden), - "metadata-only manager retained `{forbidden}`" + "manager retained `{forbidden}`" ); } } diff --git a/crates/runtime_manager/src/model.rs b/crates/runtime_manager/src/model.rs @@ -1,7 +1,6 @@ use std::collections::BTreeMap; use radroots_runtime_distribution::HardenedServiceTargets; -use radroots_runtime_paths::{InstanceId, ServiceId}; use serde::Deserialize; #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] @@ -19,17 +18,15 @@ pub struct RadrootsRuntimeManagementContract { pub service_targets: HardenedServiceTargets, pub lifecycle: LifecycleContract, pub mode: BTreeMap<String, ManagementModeContract>, - pub paths: BTreeMap<String, ManagementPathContract>, pub instance_metadata: InstanceMetadataContract, - pub bootstrap: BTreeMap<String, BootstrapRuntimeContract>, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct ManagementDefaults { pub instance_cardinality: String, - pub managed_runtime_lookup: String, - pub explicit_runtime_endpoint_overrides_precede_managed_instance_binding: bool, + pub runtime_binding: String, + pub admin_endpoint: String, pub global_path_mutation_forbidden: bool, } @@ -64,30 +61,7 @@ pub struct ManagementModeContract { #[serde(default)] pub supported_profiles: Vec<String>, pub service_manager_integration: bool, - pub uses_absolute_binary_paths: bool, pub default_instance_cardinality: String, - pub requires_explicit_pid_tracking: Option<bool>, - pub requires_explicit_log_tracking: Option<bool>, -} - -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct ManagementPathContract { - pub shared_namespace: String, - pub instance_registry_root_class: String, - pub instance_registry_rel: String, - pub artifact_cache_root_class: String, - pub artifact_cache_rel: String, - pub install_root_class: String, - pub install_root_rel: String, - pub state_root_class: String, - pub state_root_rel: String, - pub logs_root_class: String, - pub logs_root_rel: String, - pub run_root_class: String, - pub run_root_rel: String, - pub secrets_root_class: String, - pub secrets_namespace_rel: String, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] @@ -98,52 +72,3 @@ pub struct InstanceMetadataContract { #[serde(default)] pub optional_fields: Vec<String>, } - -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct BootstrapRuntimeContract { - service_id: ServiceId, - default_instance_id: InstanceId, - preferred_cli_binding: bool, -} - -impl BootstrapRuntimeContract { - #[must_use] - pub fn service_id(&self) -> &ServiceId { - &self.service_id - } - - #[must_use] - pub fn default_instance_id(&self) -> &InstanceId { - &self.default_instance_id - } - - #[must_use] - pub const fn preferred_cli_binding(&self) -> bool { - self.preferred_cli_binding - } -} - -#[cfg(test)] -mod tests { - use super::BootstrapRuntimeContract; - - #[test] - fn bootstrap_accessors_project_parsed_fields_without_selecting_a_default() { - let contract = toml::from_str::<BootstrapRuntimeContract>( - r#" -service_id = "myc" -default_instance_id = "explicit-test-instance" -preferred_cli_binding = false -"#, - ) - .expect("bootstrap contract"); - - assert_eq!(contract.service_id().as_str(), "myc"); - assert_eq!( - contract.default_instance_id().as_str(), - "explicit-test-instance" - ); - assert!(!contract.preferred_cli_binding()); - } -} diff --git a/crates/runtime_manager/src/status.rs b/crates/runtime_manager/src/status.rs @@ -0,0 +1,237 @@ +//! Context-bound status-v1 access over the shared Unix admin client. + +use core::fmt; + +use radroots_runtime_paths::{ + InstanceId, RuntimeContext, ServiceId, default_service_instance_artifacts, +}; +use radroots_service_host::{ + AdminClient, AdminClientTarget, AdminTransportLimits, SERVICE_STATUS_CONTRACT_VERSION, +}; +use serde::{Serialize, de::DeserializeOwned}; + +use crate::RadrootsRuntimeManagerError; + +const STATUS_V1_TARGET: &str = "/v1/status"; + +/// Identity projection required from a service-owned status-v1 response. +/// +/// Myc and RHI retain ownership of their typed status details. This trait lets +/// the manager validate only the shared contract and selected runtime identity +/// without accepting an untyped JSON payload. +pub trait ManagedServiceStatusV1: Serialize + DeserializeOwned { + fn contract_version(&self) -> u32; + fn service_id(&self) -> &ServiceId; + fn instance_id(&self) -> &InstanceId; +} + +/// Bounded status-v1 client sealed to one [`RuntimeContext`] admin socket. +pub struct ManagedRuntimeStatusClient { + expected_service: ServiceId, + expected_instance: InstanceId, + client: AdminClient, + target: AdminClientTarget, +} + +impl ManagedRuntimeStatusClient { + pub(crate) fn for_context( + context: &RuntimeContext, + limits: AdminTransportLimits, + ) -> Result<Self, RadrootsRuntimeManagerError> { + let artifacts = default_service_instance_artifacts(context.paths()); + let client = AdminClient::new(artifacts.admin_socket(), limits) + .map_err(|_| RadrootsRuntimeManagerError::AdminClient)?; + let target = AdminClientTarget::new(STATUS_V1_TARGET) + .map_err(|_| RadrootsRuntimeManagerError::AdminClient)?; + Ok(Self { + expected_service: context.service().clone(), + expected_instance: context.instance().clone(), + client, + target, + }) + } + + /// Requests and identity-validates the service-owned typed status model. + pub async fn get<S>(&self) -> Result<S, RadrootsRuntimeManagerError> + where + S: ManagedServiceStatusV1, + { + let status = self + .client + .get::<S>(&self.target) + .await + .map_err(|_| RadrootsRuntimeManagerError::AdminRequest)? + .into_result(); + if status.contract_version() != SERVICE_STATUS_CONTRACT_VERSION + || status.service_id() != &self.expected_service + || status.instance_id() != &self.expected_instance + { + return Err(RadrootsRuntimeManagerError::StatusContractMismatch); + } + Ok(status) + } +} + +impl fmt::Debug for ManagedRuntimeStatusClient { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ManagedRuntimeStatusClient") + .field("expected_service", &self.expected_service) + .field("expected_instance", &self.expected_instance) + .field("socket_path", &"[redacted]") + .field("target", &STATUS_V1_TARGET) + .finish() + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use radroots_runtime_paths::{ + InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, + }; + use radroots_service_host::AdminTransportLimits; + use serde::{Deserialize, Serialize}; + use tempfile::Builder; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::UnixListener, + }; + + use super::{ManagedRuntimeStatusClient, ManagedServiceStatusV1}; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + struct TestStatus { + contract_version: u32, + service: ServiceId, + instance: InstanceId, + phase: String, + } + + impl ManagedServiceStatusV1 for TestStatus { + fn contract_version(&self) -> u32 { + self.contract_version + } + + fn service_id(&self) -> &ServiceId { + &self.service + } + + fn instance_id(&self) -> &InstanceId { + &self.instance + } + } + + fn 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("context") + } + + async fn serve_status_once(socket: PathBuf, result: serde_json::Value) { + if socket.exists() { + std::fs::remove_file(&socket).expect("remove prior socket"); + } + let listener = UnixListener::bind(socket).expect("bind status socket"); + let (mut stream, _) = listener.accept().await.expect("accept status request"); + let mut request = [0_u8; 4_096]; + let read = stream.read(&mut request).await.expect("read request"); + let request = std::str::from_utf8(&request[..read]).expect("UTF-8 request"); + assert!(request.starts_with("GET /v1/status HTTP/1.1\r\n")); + + let body = serde_json::to_vec(&serde_json::json!({ + "contract_version": 1, + "ok": true, + "correlation_id": "status-test-01", + "result": result, + })) + .expect("response body"); + let head = format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n", + body.len() + ); + stream + .write_all(head.as_bytes()) + .await + .expect("write response head"); + stream.write_all(&body).await.expect("write response body"); + stream.shutdown().await.expect("shutdown response"); + } + + #[test] + fn construction_is_context_bound_and_debug_redacts_the_socket() { + let client = ManagedRuntimeStatusClient::for_context( + &context(PathBuf::from("/sensitive/project-root")), + AdminTransportLimits::DEFAULT, + ) + .expect("client"); + let debug = format!("{client:?}"); + assert!(debug.contains("/v1/status")); + assert!(!debug.contains("sensitive")); + assert!(!debug.contains("admin.sock")); + } + + #[tokio::test(flavor = "current_thread")] + async fn bounded_unix_status_round_trip_validates_the_complete_common_identity() { + let temp = Builder::new() + .prefix("rrm") + .tempdir_in("/tmp") + .expect("short temp root"); + let context = context(temp.path().to_path_buf()); + let socket = radroots_runtime_paths::default_service_instance_artifacts(context.paths()) + .admin_socket() + .to_path_buf(); + std::fs::create_dir_all(socket.parent().expect("socket parent")) + .expect("create socket parent"); + let client = + ManagedRuntimeStatusClient::for_context(&context, AdminTransportLimits::DEFAULT) + .expect("client"); + + let success_server = tokio::spawn(serve_status_once( + socket.clone(), + serde_json::json!({ + "contract_version": 1, + "service": "myc", + "instance": "primary", + "phase": "ready", + }), + )); + tokio::task::yield_now().await; + let status = client.get::<TestStatus>().await.expect("status"); + assert_eq!(status.phase, "ready"); + success_server.await.expect("success server"); + + for (field, replacement) in [ + ("contract_version", serde_json::json!(2)), + ("service", serde_json::json!("rhi")), + ("instance", serde_json::json!("secondary")), + ] { + let mut result = serde_json::json!({ + "contract_version": 1, + "service": "myc", + "instance": "primary", + "phase": "ready", + }); + result[field] = replacement; + let server = tokio::spawn(serve_status_once(socket.clone(), result)); + tokio::task::yield_now().await; + assert_eq!( + client.get::<TestStatus>().await, + Err(crate::RadrootsRuntimeManagerError::StatusContractMismatch) + ); + server.await.expect("mismatch server"); + } + } +} diff --git a/crates/runtime_manager/tests/fixtures/hardened_service_management.v1.toml b/crates/runtime_manager/tests/fixtures/hardened_service_management.v1.toml @@ -7,27 +7,26 @@ capabilities_contract = "service-capabilities.v1.toml" [defaults] instance_cardinality = "multiple" -managed_runtime_lookup = "typed_instance_registry" -explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true +runtime_binding = "typed_runtime_context" +admin_endpoint = "runtime_context_admin_socket" global_path_mutation_forbidden = true [management_clients] -active = ["cli"] +active = ["cli_v1", "unix_admin_v1"] [managed_runtime_targets] -defined = ["myc", "rhi"] +active = ["myc", "rhi"] [lifecycle] -actions = [] +actions = ["config_init", "config_validate", "state_init", "run", "status", "doctor"] destructive_actions = [] -health_states = [] +health_states = ["starting", "ready", "degraded", "unready", "stopping", "failed"] [mode.interactive_user_managed] contract_state = "active" platforms = ["linux", "macos"] supported_profiles = ["interactive", "repo_local"] service_manager_integration = false -uses_absolute_binary_paths = true default_instance_cardinality = "multiple" [mode.service_host_managed] @@ -35,28 +34,10 @@ contract_state = "active" platforms = ["linux"] supported_profiles = ["service_host"] service_manager_integration = true -uses_absolute_binary_paths = true default_instance_cardinality = "multiple" -[paths.context_bound] -shared_namespace = "services" -instance_registry_root_class = "config" -instance_registry_rel = "instances.toml" -artifact_cache_root_class = "cache" -artifact_cache_rel = "artifacts" -install_root_class = "state" -install_root_rel = "installs" -state_root_class = "state" -state_root_rel = "state.sqlite" -logs_root_class = "logs" -logs_root_rel = "instances" -run_root_class = "run" -run_root_rel = "admin.sock" -secrets_root_class = "secrets" -secrets_namespace_rel = "credentials" - [instance_metadata] -required_fields = ["service_id", "instance_id"] +required_fields = ["service_id", "instance_id", "profile"] optional_fields = [] [service_targets.myc] @@ -86,5 +67,3 @@ status_surface = "local_admin_service_status_v1" operations_surface = "cached_livez_readyz_metrics" support_posture = "target" tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] - -[bootstrap] diff --git a/crates/runtime_manager/tests/service_target_boundary.rs b/crates/runtime_manager/tests/service_target_boundary.rs @@ -1,7 +1,10 @@ const MANAGEMENT_FIXTURE: &str = include_str!("fixtures/hardened_service_management.v1.toml"); const MANAGER_ROOT_SOURCE: &str = include_str!("../src/lib.rs"); const MANAGER_SOURCE: &str = include_str!("../src/managed.rs"); +const CLI_SOURCE: &str = include_str!("../src/cli.rs"); const MODEL_SOURCE: &str = include_str!("../src/model.rs"); +#[cfg(any(target_os = "linux", target_os = "macos"))] +const STATUS_SOURCE: &str = include_str!("../src/status.rs"); const MANIFEST: &str = include_str!("../Cargo.toml"); const README: &str = include_str!("../README"); @@ -13,16 +16,19 @@ fn production_source(source: &str) -> &str { } #[test] -fn hardened_services_remain_exact_metadata_only_targets() { +fn hardened_services_use_only_the_common_context_bound_interfaces() { for forbidden in [ - "active = [\"myc", - "active = [\"rhi", "install_strategy", "binary_name", "artifact_adapter", "qualified", "default_instance_id", "preferred_cli_binding", + "typed_instance_registry", + "instances.toml", + "explicit_runtime_endpoint_overrides", + "[paths.", + "[bootstrap]", ] { assert!( !MANAGEMENT_FIXTURE.contains(forbidden), @@ -30,10 +36,14 @@ fn hardened_services_remain_exact_metadata_only_targets() { ); } - assert!(MANAGEMENT_FIXTURE.contains("defined = [\"myc\", \"rhi\"]")); - assert!(MANAGEMENT_FIXTURE.contains("actions = []")); + assert!(MANAGEMENT_FIXTURE.contains("active = [\"myc\", \"rhi\"]")); + assert!(MANAGEMENT_FIXTURE.contains("active = [\"cli_v1\", \"unix_admin_v1\"]")); + assert!(MANAGEMENT_FIXTURE.contains( + "actions = [\"config_init\", \"config_validate\", \"state_init\", \"run\", \"status\", \"doctor\"]" + )); assert!(MANAGEMENT_FIXTURE.contains("destructive_actions = []")); - assert!(MANAGEMENT_FIXTURE.contains("[bootstrap]")); + assert!(MANAGEMENT_FIXTURE.contains("runtime_binding = \"typed_runtime_context\"")); + assert!(MANAGEMENT_FIXTURE.contains("admin_endpoint = \"runtime_context_admin_socket\"")); } #[test] @@ -54,18 +64,19 @@ fn management_contract_is_bounded_before_toml_admission() { } #[test] -fn public_package_contains_only_metadata_resolution_authority() { - let production = [ +fn public_package_contains_only_typed_cli_and_bounded_admin_authority() { + let sources = [ production_source(MANAGER_ROOT_SOURCE), production_source(MANAGER_SOURCE), + production_source(CLI_SOURCE), production_source(MODEL_SOURCE), - ] - .join("\n"); + #[cfg(any(target_os = "linux", target_os = "macos"))] + production_source(STATUS_SOURCE), + ]; + let production = sources.join("\n"); for forbidden in [ "std::fs", "std::process", - "std::path", - "radroots_runtime_paths::RuntimeContext", "ManagedRuntimeArtifactName", "ManagedRuntimeInstancePaths", "ManagedRuntimeSharedPaths", @@ -83,6 +94,11 @@ fn public_package_contains_only_metadata_resolution_authority() { "remove_instance_artifacts", "write_instance_config", "inspect_runtime_", + "read_secret", + "credential_path", + "binary_name", + "archive_name", + "install_path", ] { assert!( !production.contains(forbidden), @@ -90,7 +106,7 @@ fn public_package_contains_only_metadata_resolution_authority() { ); } - for forbidden_dependency in ["flate2", "tar =", "tempfile"] { + for forbidden_dependency in ["flate2", "tar ="] { assert!( !MANIFEST.contains(forbidden_dependency), "manifest retained `{forbidden_dependency}`" @@ -98,10 +114,26 @@ fn public_package_contains_only_metadata_resolution_authority() { } for required in [ - "performs no filesystem, registry, process, archive, artifact", - "runtime paths or raw persistence helpers", - "Steps 219 and 220", + "sole service, instance,\nprofile, and canonical-path authority", + "exact CLI-v1 argument\nplans", + "fixed `/v1/status`", + "never discovers or executes a program", + "Artifact and\ndistribution resolution remains separately governed by Step220", ] { assert!(README.contains(required), "README omitted `{required}`"); } + + for required in ["RuntimeContext", "ManagedCliInvocation"] { + assert!( + production.contains(required), + "production omitted `{required}`" + ); + } + #[cfg(any(target_os = "linux", target_os = "macos"))] + for required in ["AdminClient", "AdminClientTarget", "STATUS_V1_TARGET"] { + assert!( + production.contains(required), + "native production omitted `{required}`" + ); + } } diff --git a/crates/runtime_paths/src/context.rs b/crates/runtime_paths/src/context.rs @@ -1,7 +1,7 @@ //! Immutable resolved bootstrap context for one service instance. use core::fmt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use serde::{Serialize, Serializer, ser::SerializeStruct}; use thiserror::Error; @@ -137,6 +137,7 @@ impl RuntimeContextSources { /// service: todo!(), /// instance: todo!(), /// profile: todo!(), +/// repo_local_root: todo!(), /// paths: todo!(), /// sources: todo!(), /// }; @@ -146,6 +147,7 @@ pub struct RuntimeContext { service: ServiceId, instance: InstanceId, profile: RadrootsPathProfile, + repo_local_root: Option<PathBuf>, paths: RadrootsServiceInstancePaths, sources: RuntimeContextSources, } @@ -157,16 +159,21 @@ impl RuntimeContext { service: ServiceId, instance: InstanceId, ) -> Result<Self, RuntimeContextError> { + let RuntimeContextBootstrap { + profile, + repo_local_root, + profile_source, + instance_source, + } = bootstrap; let roots = resolver - .resolve(bootstrap.profile, bootstrap.repo_local_root.as_deref()) + .resolve(profile, repo_local_root.as_deref()) .map_err(|_| RuntimeContextError::PathSelection)?; let paths = RadrootsServiceInstancePaths::from_resolved_roots(&roots, &service, &instance); let sources = RuntimeContextSources { service: RuntimeContextSource::SafeDefault, - instance: bootstrap.instance_source, - profile: bootstrap.profile_source, - repo_local_root: bootstrap - .repo_local_root + instance: instance_source, + profile: profile_source, + repo_local_root: repo_local_root .as_ref() .map(|_| RuntimeContextSource::BootstrapCli), paths: RuntimeContextSource::DerivedPath, @@ -175,7 +182,8 @@ impl RuntimeContext { Ok(Self { service, instance, - profile: bootstrap.profile, + profile, + repo_local_root, paths, sources, }) @@ -196,6 +204,12 @@ impl RuntimeContext { self.profile } + /// Returns the validated explicit repo-local base when that profile is active. + #[must_use] + pub fn repo_local_root(&self) -> Option<&Path> { + self.repo_local_root.as_deref() + } + #[must_use] pub fn paths(&self) -> &RadrootsServiceInstancePaths { &self.paths @@ -214,6 +228,10 @@ impl fmt::Debug for RuntimeContext { .field("service", &self.service) .field("instance", &self.instance) .field("profile", &self.profile) + .field( + "repo_local_root", + &self.repo_local_root.as_ref().map(|_| "[redacted]"), + ) .field("paths", &"[redacted]") .field("sources", &self.sources) .finish() @@ -285,6 +303,7 @@ mod tests { for (service, instance) in [("myc", "primary"), ("myc", "secondary"), ("rhi", "default")] { let context = repo_local_context_for(base.clone(), service, instance); + assert_eq!(context.repo_local_root(), Some(base.as_path())); let suffix = PathBuf::from("services").join(service).join(instance); assert_eq!(context.paths().config(), base.join("config").join(&suffix)); assert_eq!(context.paths().state(), base.join("data").join(&suffix)); @@ -588,6 +607,7 @@ mod tests { RuntimeContextSource::SafeDefault ); assert_eq!(defaulted.sources().repo_local_root(), None); + assert_eq!(defaulted.repo_local_root(), None); } #[test] diff --git a/crates/runtime_paths/tests/package_boundary.rs b/crates/runtime_paths/tests/package_boundary.rs @@ -124,6 +124,7 @@ fn reviewed_api_requires_the_typed_runtime_context_boundary() { "pub struct radroots_runtime_paths::ServiceId", "pub struct radroots_runtime_paths::InstanceId", "pub fn radroots_runtime_paths::RuntimeContext::resolve", + "pub fn radroots_runtime_paths::RuntimeContext::repo_local_root", "pub fn radroots_runtime_paths::RadrootsPlatform::current", "pub fn radroots_runtime_paths::default_service_instance_artifacts", "pub fn radroots_runtime_paths::service_credential_artifact_path",