commit fdad8c0bae79b21938963670c43497fb4ffb826e
parent da29d88e8f0b8316fa91287d6a7ef184a57459c0
Author: triesap <tyson@radroots.org>
Date: Sun, 12 Apr 2026 04:37:39 +0000
runtime_manager: add managed runtime facade helpers
Diffstat:
4 files changed, 559 insertions(+), 10 deletions(-)
diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs
@@ -2,6 +2,7 @@
pub mod error;
pub mod lifecycle;
+pub mod managed;
pub mod model;
pub mod paths;
pub mod registry;
@@ -12,6 +13,10 @@ pub use lifecycle::{
read_secret_file, remove_instance_artifacts, start_process, stop_process,
write_instance_metadata, write_managed_file, write_secret_file,
};
+pub use managed::{
+ active_management_mode_for_profile, load_management_context, resolve_runtime_target,
+ runtime_group, ManagedRuntimeContext, ManagedRuntimeGroup, ManagedRuntimeTarget,
+};
pub use model::{
BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState,
ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry,
@@ -19,8 +24,8 @@ pub use model::{
RadrootsRuntimeManagementContract, RuntimeGroups,
};
pub use paths::{
- ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime,
- resolve_instance_paths, resolve_shared_paths,
+ bootstrap_runtime, resolve_instance_paths, resolve_shared_paths, ManagedRuntimeInstancePaths,
+ ManagedRuntimeSharedPaths,
};
pub use registry::{instance, load_registry, remove_instance, save_registry, upsert_instance};
@@ -51,9 +56,9 @@ mod tests {
use tempfile::tempdir;
use crate::{
- ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord,
bootstrap_runtime, instance, load_registry, parse_contract_str, resolve_instance_paths,
- resolve_shared_paths, save_registry, upsert_instance,
+ resolve_shared_paths, save_registry, upsert_instance, ManagedRuntimeHealthState,
+ ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord,
};
fn assert_error_contains(err: &crate::RadrootsRuntimeManagerError, parts: &[&str]) {
diff --git a/crates/runtime_manager/src/managed.rs b/crates/runtime_manager/src/managed.rs
@@ -0,0 +1,326 @@
+use std::path::PathBuf;
+
+use radroots_runtime_paths::{RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver};
+
+use crate::{
+ load_registry, resolve_instance_paths, resolve_shared_paths, BootstrapRuntimeContract,
+ ManagedRuntimeInstancePaths, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry,
+ ManagementModeContract, RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError,
+};
+
+#[derive(Debug, Clone)]
+pub struct ManagedRuntimeContext {
+ pub contract: RadrootsRuntimeManagementContract,
+ pub shared_paths: crate::ManagedRuntimeSharedPaths,
+ pub registry: ManagedRuntimeInstanceRegistry,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ManagedRuntimeGroup {
+ ActiveManagedTarget,
+ DefinedManagedTarget,
+ BootstrapOnly,
+ Unknown,
+}
+
+impl ManagedRuntimeGroup {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::ActiveManagedTarget => "active_managed_target",
+ Self::DefinedManagedTarget => "defined_managed_target",
+ Self::BootstrapOnly => "bootstrap_only",
+ Self::Unknown => "unknown",
+ }
+ }
+
+ pub fn posture(self) -> &'static str {
+ match self {
+ Self::ActiveManagedTarget => "active_managed_target",
+ Self::DefinedManagedTarget => "defined_future_target",
+ Self::BootstrapOnly => "bootstrap_only_direct_binding",
+ Self::Unknown => "unknown_runtime",
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct ManagedRuntimeTarget {
+ pub runtime_id: String,
+ pub instance_id: String,
+ pub instance_source: String,
+ pub runtime_group: ManagedRuntimeGroup,
+ pub management_mode: Option<String>,
+ pub mode_contract: Option<ManagementModeContract>,
+ pub bootstrap: Option<BootstrapRuntimeContract>,
+ pub instance_record: Option<ManagedRuntimeInstanceRecord>,
+ pub predicted_paths: Option<ManagedRuntimeInstancePaths>,
+ pub registry_path: PathBuf,
+}
+
+pub fn load_management_context(
+ contract: RadrootsRuntimeManagementContract,
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+) -> Result<ManagedRuntimeContext, RadrootsRuntimeManagerError> {
+ let mode_id = active_management_mode_for_profile(&contract, profile)?;
+ let shared_paths = resolve_shared_paths(&contract, resolver, profile, overrides, mode_id)?;
+ let registry = load_registry(&shared_paths.instance_registry_path)?;
+ Ok(ManagedRuntimeContext {
+ contract,
+ shared_paths,
+ registry,
+ })
+}
+
+pub fn active_management_mode_for_profile<'a>(
+ contract: &'a RadrootsRuntimeManagementContract,
+ profile: RadrootsPathProfile,
+) -> Result<&'a str, RadrootsRuntimeManagerError> {
+ let profile_id = profile.to_string();
+ contract
+ .mode
+ .iter()
+ .find(|(_, mode)| {
+ mode.contract_state == "active"
+ && mode
+ .supported_profiles
+ .iter()
+ .any(|entry| entry == &profile_id)
+ })
+ .map(|(mode_id, _)| mode_id.as_str())
+ .ok_or_else(|| RadrootsRuntimeManagerError::UnsupportedProfile {
+ mode_id: "active".to_owned(),
+ profile: profile_id,
+ })
+}
+
+pub fn resolve_runtime_target(
+ context: &ManagedRuntimeContext,
+ runtime_id: &str,
+ requested_instance_id: Option<&str>,
+) -> ManagedRuntimeTarget {
+ let runtime_group = runtime_group(&context.contract, runtime_id);
+ let bootstrap = context.contract.bootstrap.get(runtime_id).cloned();
+ let instance_id = requested_instance_id
+ .map(ToOwned::to_owned)
+ .or_else(|| {
+ bootstrap
+ .as_ref()
+ .map(|entry| entry.default_instance_id.clone())
+ })
+ .unwrap_or_else(|| "default".to_owned());
+ let instance_source = if requested_instance_id.is_some() {
+ "command_arg".to_owned()
+ } else if bootstrap.is_some() {
+ "bootstrap_default".to_owned()
+ } else {
+ "implicit_default".to_owned()
+ };
+ let management_mode = bootstrap
+ .as_ref()
+ .map(|entry| entry.management_mode.clone());
+ let mode_contract = management_mode
+ .as_ref()
+ .and_then(|mode_id| context.contract.mode.get(mode_id).cloned());
+ let instance_record = context
+ .registry
+ .instances
+ .iter()
+ .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)
+ .cloned();
+ let predicted_paths = if runtime_group == ManagedRuntimeGroup::ActiveManagedTarget {
+ Some(resolve_instance_paths(
+ &context.shared_paths,
+ runtime_id,
+ instance_id.as_str(),
+ ))
+ } else {
+ None
+ };
+
+ ManagedRuntimeTarget {
+ runtime_id: runtime_id.to_owned(),
+ instance_id,
+ instance_source,
+ runtime_group,
+ management_mode,
+ mode_contract,
+ bootstrap,
+ instance_record,
+ predicted_paths,
+ registry_path: context.shared_paths.instance_registry_path.clone(),
+ }
+}
+
+pub fn runtime_group(
+ contract: &RadrootsRuntimeManagementContract,
+ runtime_id: &str,
+) -> ManagedRuntimeGroup {
+ if contract
+ .managed_runtime_targets
+ .active
+ .iter()
+ .any(|entry| entry == runtime_id)
+ {
+ ManagedRuntimeGroup::ActiveManagedTarget
+ } else if contract
+ .managed_runtime_targets
+ .defined
+ .iter()
+ .any(|entry| entry == runtime_id)
+ {
+ ManagedRuntimeGroup::DefinedManagedTarget
+ } else if contract
+ .managed_runtime_targets
+ .bootstrap_only
+ .iter()
+ .any(|entry| entry == runtime_id)
+ {
+ ManagedRuntimeGroup::BootstrapOnly
+ } else {
+ ManagedRuntimeGroup::Unknown
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform,
+ };
+
+ use super::{
+ active_management_mode_for_profile, load_management_context, resolve_runtime_target,
+ ManagedRuntimeGroup,
+ };
+ use crate::{parse_contract_str, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord};
+
+ const CONTRACT: &str = r#"
+schema = "radroots-runtime-management"
+schema_version = 1
+owner_doc = "docs/migration/radroots-modular-runtime-management-bootstrap-rcl.md"
+runtime_registry = "registry.toml"
+distribution_contract = "distribution.toml"
+capabilities_contract = "capabilities.toml"
+
+[defaults]
+instance_cardinality = "single_default_instance"
+managed_runtime_lookup = "shared_instance_registry"
+explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true
+global_path_mutation_forbidden = true
+
+[management_clients]
+active = ["cli"]
+defined = []
+
+[managed_runtime_targets]
+active = ["radrootsd"]
+defined = ["myc"]
+bootstrap_only = ["hyf"]
+
+[lifecycle]
+actions = ["install", "start"]
+destructive_actions = []
+health_states = ["not_installed", "running"]
+
+[mode.interactive_user_managed]
+contract_state = "active"
+platforms = ["linux"]
+supported_profiles = ["interactive_user", "repo_local"]
+service_manager_integration = false
+uses_absolute_binary_paths = true
+default_instance_cardinality = "single_default_instance"
+
+[paths.interactive_user_managed]
+shared_namespace = "shared/runtime-manager"
+instance_registry_root_class = "config"
+instance_registry_rel = "shared/runtime-manager/instances.toml"
+artifact_cache_root_class = "cache"
+artifact_cache_rel = "shared/runtime-manager/artifacts"
+install_root_class = "data"
+install_root_rel = "shared/runtime-manager/installs"
+state_root_class = "data"
+state_root_rel = "shared/runtime-manager/state"
+logs_root_class = "logs"
+logs_root_rel = "shared/runtime-manager"
+run_root_class = "run"
+run_root_rel = "shared/runtime-manager"
+secrets_root_class = "secrets"
+secrets_namespace_rel = "shared/runtime-manager"
+
+[instance_metadata]
+required_fields = ["runtime_id"]
+optional_fields = ["notes"]
+
+[bootstrap.radrootsd]
+runtime_id = "radrootsd"
+management_mode = "interactive_user_managed"
+default_instance_id = "local"
+install_strategy = "archive_unpack"
+config_format = "toml"
+requires_bootstrap_secret = true
+requires_config_bootstrap = true
+requires_signer_provider = false
+health_surface = "jsonrpc_status"
+preferred_cli_binding = true
+"#;
+
+ #[test]
+ fn active_management_mode_matches_supported_profile() {
+ let contract = parse_contract_str(CONTRACT).expect("contract");
+ let mode_id =
+ active_management_mode_for_profile(&contract, RadrootsPathProfile::InteractiveUser)
+ .expect("mode");
+ assert_eq!(mode_id, "interactive_user_managed");
+ }
+
+ #[test]
+ fn resolve_runtime_target_uses_bootstrap_default_instance_id() {
+ let contract = parse_contract_str(CONTRACT).expect("contract");
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+ let mut context = load_management_context(
+ contract,
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+ .expect("context");
+ context
+ .registry
+ .instances
+ .push(ManagedRuntimeInstanceRecord {
+ runtime_id: "radrootsd".to_owned(),
+ instance_id: "local".to_owned(),
+ management_mode: "interactive_user_managed".to_owned(),
+ install_state: ManagedRuntimeInstallState::Configured,
+ binary_path: PathBuf::from("/tmp/bin/radrootsd"),
+ config_path: PathBuf::from("/tmp/config.toml"),
+ logs_path: PathBuf::from("/tmp/logs"),
+ run_path: PathBuf::from("/tmp/run"),
+ installed_version: "0.1.0-alpha.2".to_owned(),
+ health_endpoint: None,
+ secret_material_ref: None,
+ last_started_at: None,
+ last_stopped_at: None,
+ notes: None,
+ });
+
+ let target = resolve_runtime_target(&context, "radrootsd", None);
+ assert_eq!(target.instance_id, "local");
+ assert_eq!(target.instance_source, "bootstrap_default");
+ assert_eq!(
+ target.runtime_group,
+ ManagedRuntimeGroup::ActiveManagedTarget
+ );
+ assert!(target.predicted_paths.is_some());
+ }
+}
diff --git a/crates/runtime_paths/src/lib.rs b/crates/runtime_paths/src/lib.rs
@@ -6,21 +6,22 @@ pub mod migration;
pub mod namespace;
pub mod platform;
pub mod roots;
+pub mod service;
pub use conventions::{
- DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME,
- DEFAULT_SHARED_IDENTITY_FILE_NAME, RadrootsBootstrapPaths, default_namespaced_bootstrap_paths,
- default_shared_identity_path, default_shared_runtime_logs_dir,
+ default_namespaced_bootstrap_paths, default_shared_identity_path,
+ default_shared_runtime_logs_dir, RadrootsBootstrapPaths, DEFAULT_CONFIG_FILE_NAME,
+ DEFAULT_SERVICE_IDENTITY_FILE_NAME, DEFAULT_SHARED_IDENTITY_FILE_NAME,
};
pub use error::RadrootsRuntimePathsError;
pub use migration::{
- RADROOTS_MIGRATION_COMPATIBILITY_WINDOW, RADROOTS_MIGRATION_POSTURE,
- RadrootsLegacyPathCandidate, RadrootsLegacyPathDetection, RadrootsMigrationReport,
- inspect_legacy_paths,
+ inspect_legacy_paths, RadrootsLegacyPathCandidate, RadrootsLegacyPathDetection,
+ RadrootsMigrationReport, RADROOTS_MIGRATION_COMPATIBILITY_WINDOW, RADROOTS_MIGRATION_POSTURE,
};
pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind};
pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform};
pub use roots::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths};
+pub use service::{RadrootsRuntimePathSelection, RadrootsRuntimePathSelectionError};
#[cfg(test)]
mod tests {
diff --git a/crates/runtime_paths/src/service.rs b/crates/runtime_paths/src/service.rs
@@ -0,0 +1,217 @@
+use std::path::PathBuf;
+
+use thiserror::Error;
+
+use crate::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths,
+ RadrootsRuntimeNamespace, RadrootsRuntimePathsError,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsRuntimePathSelection {
+ pub profile: RadrootsPathProfile,
+ pub profile_source: String,
+ pub repo_local_root: Option<PathBuf>,
+ pub repo_local_root_source: Option<String>,
+}
+
+#[derive(Debug, Error, Clone, PartialEq, Eq)]
+pub enum RadrootsRuntimePathSelectionError {
+ #[error("{env_var} must be valid utf-8 when set")]
+ NonUnicodeEnv { env_var: String },
+
+ #[error(
+ "{env_var} must be `interactive_user`, `service_host`, or `repo_local`; found `{value}`"
+ )]
+ InvalidProfileEnv { env_var: String, value: String },
+
+ #[error("{repo_local_root_env} must be set when {profile_env}=repo_local")]
+ MissingRepoLocalRoot {
+ profile_env: String,
+ repo_local_root_env: String,
+ },
+
+ #[error(transparent)]
+ Paths(#[from] RadrootsRuntimePathsError),
+}
+
+impl RadrootsRuntimePathSelection {
+ pub fn caller(profile: RadrootsPathProfile, repo_local_root: Option<PathBuf>) -> Self {
+ Self {
+ profile,
+ profile_source: "caller".to_owned(),
+ repo_local_root_source: repo_local_root.as_ref().map(|_| "caller".to_owned()),
+ repo_local_root,
+ }
+ }
+
+ pub fn from_env(
+ profile_env: &'static str,
+ repo_local_root_env: &'static str,
+ default_profile: RadrootsPathProfile,
+ ) -> Result<Self, RadrootsRuntimePathSelectionError> {
+ let (profile, profile_source) = match std::env::var(profile_env) {
+ Ok(value) => (
+ parse_profile(profile_env, value.as_str())?,
+ format!("process_env:{profile_env}"),
+ ),
+ Err(std::env::VarError::NotPresent) => (default_profile, "default".to_owned()),
+ Err(std::env::VarError::NotUnicode(_)) => {
+ return Err(RadrootsRuntimePathSelectionError::NonUnicodeEnv {
+ env_var: profile_env.to_owned(),
+ });
+ }
+ };
+ let repo_local_root_raw = std::env::var_os(repo_local_root_env);
+ let repo_local_root = repo_local_root_raw.as_ref().map(PathBuf::from);
+ Ok(Self {
+ profile,
+ profile_source,
+ repo_local_root,
+ repo_local_root_source: repo_local_root_raw
+ .as_ref()
+ .map(|_| format!("process_env:{repo_local_root_env}")),
+ })
+ }
+
+ pub fn root_source(&self) -> &'static str {
+ match self.profile {
+ RadrootsPathProfile::InteractiveUser => "host_defaults",
+ RadrootsPathProfile::ServiceHost => "service_host_defaults",
+ RadrootsPathProfile::RepoLocal => "repo_local_root",
+ RadrootsPathProfile::MobileNative => "mobile_native_defaults",
+ }
+ }
+
+ pub fn overrides(
+ &self,
+ profile_env: &'static str,
+ repo_local_root_env: &'static str,
+ ) -> Result<RadrootsPathOverrides, RadrootsRuntimePathSelectionError> {
+ match self.profile {
+ RadrootsPathProfile::RepoLocal => {
+ let Some(repo_local_root) = self.repo_local_root.as_ref() else {
+ return Err(RadrootsRuntimePathSelectionError::MissingRepoLocalRoot {
+ profile_env: profile_env.to_owned(),
+ repo_local_root_env: repo_local_root_env.to_owned(),
+ });
+ };
+ Ok(RadrootsPathOverrides::repo_local(repo_local_root))
+ }
+ _ => Ok(RadrootsPathOverrides::default()),
+ }
+ }
+
+ pub fn resolve_service_roots(
+ &self,
+ resolver: &RadrootsPathResolver,
+ service_id: &str,
+ profile_env: &'static str,
+ repo_local_root_env: &'static str,
+ ) -> Result<RadrootsPaths, RadrootsRuntimePathSelectionError> {
+ let namespace = RadrootsRuntimeNamespace::service(service_id)?;
+ let overrides = self.overrides(profile_env, repo_local_root_env)?;
+ let roots = resolver.resolve(self.profile, &overrides)?;
+ Ok(roots.namespaced(&namespace))
+ }
+}
+
+fn parse_profile(
+ env_var: &'static str,
+ value: &str,
+) -> Result<RadrootsPathProfile, RadrootsRuntimePathSelectionError> {
+ match value {
+ "interactive_user" => Ok(RadrootsPathProfile::InteractiveUser),
+ "service_host" => Ok(RadrootsPathProfile::ServiceHost),
+ "repo_local" => Ok(RadrootsPathProfile::RepoLocal),
+ other => Err(RadrootsRuntimePathSelectionError::InvalidProfileEnv {
+ env_var: env_var.to_owned(),
+ value: other.to_owned(),
+ }),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use crate::{
+ RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform,
+ };
+
+ use super::{RadrootsRuntimePathSelection, RadrootsRuntimePathSelectionError};
+
+ #[test]
+ fn caller_selection_preserves_profile_and_sources() {
+ let selection =
+ RadrootsRuntimePathSelection::caller(RadrootsPathProfile::InteractiveUser, None);
+ assert_eq!(selection.profile, RadrootsPathProfile::InteractiveUser);
+ assert_eq!(selection.profile_source, "caller");
+ assert_eq!(selection.repo_local_root, None);
+ assert_eq!(selection.repo_local_root_source, None);
+ assert_eq!(selection.root_source(), "host_defaults");
+ }
+
+ #[test]
+ fn caller_selection_marks_repo_local_source() {
+ let selection = RadrootsRuntimePathSelection::caller(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from("/repo/.local/radroots")),
+ );
+
+ assert_eq!(selection.profile_source, "caller");
+ assert_eq!(selection.repo_local_root_source.as_deref(), Some("caller"));
+ assert_eq!(selection.root_source(), "repo_local_root");
+ }
+
+ #[test]
+ fn resolve_service_roots_uses_repo_local_override() {
+ let selection = RadrootsRuntimePathSelection::caller(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from("/repo/.local/radroots")),
+ );
+ let resolver =
+ RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
+
+ let roots = selection
+ .resolve_service_roots(&resolver, "radrootsd", "PROFILE_ENV", "ROOT_ENV")
+ .expect("service roots");
+
+ assert_eq!(
+ roots.config,
+ PathBuf::from("/repo/.local/radroots/config/services/radrootsd")
+ );
+ assert_eq!(
+ roots.data,
+ PathBuf::from("/repo/.local/radroots/data/services/radrootsd")
+ );
+ assert_eq!(
+ roots.logs,
+ PathBuf::from("/repo/.local/radroots/logs/services/radrootsd")
+ );
+ assert_eq!(
+ roots.run,
+ PathBuf::from("/repo/.local/radroots/run/services/radrootsd")
+ );
+ assert_eq!(
+ roots.secrets,
+ PathBuf::from("/repo/.local/radroots/secrets/services/radrootsd")
+ );
+ }
+
+ #[test]
+ fn overrides_require_repo_local_root_for_repo_local_profile() {
+ let selection = RadrootsRuntimePathSelection::caller(RadrootsPathProfile::RepoLocal, None);
+ let err = selection
+ .overrides("RADROOTS_TEST_PROFILE", "RADROOTS_TEST_ROOT")
+ .expect_err("repo local root");
+
+ assert_eq!(
+ err,
+ RadrootsRuntimePathSelectionError::MissingRepoLocalRoot {
+ profile_env: "RADROOTS_TEST_PROFILE".to_owned(),
+ repo_local_root_env: "RADROOTS_TEST_ROOT".to_owned(),
+ }
+ );
+ }
+}