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:
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",