lib

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

commit fb14849faafd002aeb14ea3b19f84067a39b64e1
parent d9a3493a4bc65b8187b9d52de420e4a5e44e8da0
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 17:40:46 +0000

runtime_manager: move shared runtime inspection into rr-rs

Diffstat:
MCargo.lock | 3+++
Mcrates/runtime_manager/src/error.rs | 4+++-
Mcrates/runtime_manager/src/lib.rs | 16++++++++++------
Mcrates/runtime_manager/src/managed.rs | 516++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/runtime_paths/Cargo.toml | 1+
Mcrates/runtime_paths/src/lib.rs | 18++++++++++++------
Mcrates/runtime_paths/src/service.rs | 249++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
7 files changed, 782 insertions(+), 25 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2484,6 +2484,7 @@ dependencies = [ "hex", "nostr", "radroots_identity", + "radroots_nostr", "radroots_nostr_connect", "radroots_runtime", "radroots_sql_core", @@ -2505,6 +2506,7 @@ dependencies = [ "radroots_secret_vault", "serde", "serde_json", + "tempfile", "zeroize", ] @@ -2631,6 +2633,7 @@ dependencies = [ name = "radroots_runtime_paths" version = "0.1.0-alpha.2" dependencies = [ + "serde", "thiserror 1.0.69", ] diff --git a/crates/runtime_manager/src/error.rs b/crates/runtime_manager/src/error.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use radroots_runtime_paths::RadrootsRuntimePathsError; +use radroots_runtime_paths::{RadrootsRuntimePathSelectionError, RadrootsRuntimePathsError}; use thiserror::Error; #[derive(Debug, Error)] @@ -121,4 +121,6 @@ pub enum RadrootsRuntimeManagerError { }, #[error(transparent)] RuntimePaths(#[from] RadrootsRuntimePathsError), + #[error(transparent)] + RuntimePathSelection(#[from] RadrootsRuntimePathSelectionError), } diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs @@ -14,8 +14,12 @@ pub use lifecycle::{ 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, + ManagedRuntimeActionInspection, ManagedRuntimeConfigInspection, ManagedRuntimeContext, + ManagedRuntimeGroup, ManagedRuntimeInspection, ManagedRuntimeInspectionAvailability, + ManagedRuntimeLifecycleAction, ManagedRuntimeLogsInspection, ManagedRuntimeStatusInspection, + ManagedRuntimeTarget, active_management_mode_for_profile, inspect_runtime_action, + inspect_runtime_config, inspect_runtime_logs, inspect_runtime_status, load_management_context, + load_management_context_with_selection, resolve_runtime_target, runtime_group, }; pub use model::{ BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState, @@ -24,8 +28,8 @@ pub use model::{ RadrootsRuntimeManagementContract, RuntimeGroups, }; pub use paths::{ - bootstrap_runtime, resolve_instance_paths, resolve_shared_paths, ManagedRuntimeInstancePaths, - ManagedRuntimeSharedPaths, + ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime, + resolve_instance_paths, resolve_shared_paths, }; pub use registry::{instance, load_registry, remove_instance, save_registry, upsert_instance}; @@ -56,9 +60,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, ManagedRuntimeHealthState, - ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, + resolve_shared_paths, save_registry, upsert_instance, }; 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 @@ -1,11 +1,14 @@ use std::path::PathBuf; -use radroots_runtime_paths::{RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver}; +use radroots_runtime_paths::{ + RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimePathSelection, +}; use crate::{ - load_registry, resolve_instance_paths, resolve_shared_paths, BootstrapRuntimeContract, + BootstrapRuntimeContract, ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstancePaths, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, ManagementModeContract, RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError, + load_registry, resolve_instance_paths, resolve_shared_paths, }; #[derive(Debug, Clone)] @@ -57,6 +60,111 @@ pub struct ManagedRuntimeTarget { pub registry_path: PathBuf, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ManagedRuntimeInspectionAvailability { + Success, + Unconfigured, + Unsupported, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeInspection<T> { + pub availability: ManagedRuntimeInspectionAvailability, + pub view: T, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ManagedRuntimeLifecycleAction { + Install, + Uninstall, + Start, + Stop, + Restart, + ConfigSet, +} + +impl ManagedRuntimeLifecycleAction { + pub fn as_str(self) -> &'static str { + match self { + Self::Install => "install", + Self::Uninstall => "uninstall", + Self::Start => "start", + Self::Stop => "stop", + Self::Restart => "restart", + Self::ConfigSet => "config_set", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeStatusInspection { + pub runtime_id: String, + pub instance_id: String, + pub instance_source: String, + pub runtime_group: String, + pub management_posture: String, + pub state: String, + pub source: String, + pub detail: String, + pub management_mode: Option<String>, + pub service_manager_integration: Option<bool>, + pub uses_absolute_binary_paths: Option<bool>, + pub preferred_cli_binding: Option<bool>, + pub install_state: String, + pub health_state: String, + pub health_source: String, + pub registry_path: PathBuf, + pub lifecycle_actions: Vec<String>, + pub instance_paths: Option<ManagedRuntimeInstancePaths>, + pub instance_record: Option<ManagedRuntimeInstanceRecord>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeLogsInspection { + pub runtime_id: String, + pub instance_id: String, + pub instance_source: String, + pub runtime_group: String, + pub state: String, + pub source: String, + pub detail: String, + pub stdout_log_path: Option<PathBuf>, + pub stderr_log_path: Option<PathBuf>, + pub stdout_log_present: bool, + pub stderr_log_present: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeConfigInspection { + pub runtime_id: String, + pub instance_id: String, + pub instance_source: String, + pub runtime_group: String, + pub state: String, + pub source: String, + pub detail: String, + pub config_format: Option<String>, + pub config_path: Option<PathBuf>, + pub config_present: bool, + pub requires_bootstrap_secret: Option<bool>, + pub requires_config_bootstrap: Option<bool>, + pub requires_signer_provider: Option<bool>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ManagedRuntimeActionInspection { + pub action: String, + pub runtime_id: String, + pub instance_id: String, + pub instance_source: String, + pub runtime_group: String, + pub state: String, + pub source: String, + pub detail: String, + pub mutates_bindings: bool, + pub next_step: Option<String>, +} + pub fn load_management_context( contract: RadrootsRuntimeManagementContract, resolver: &RadrootsPathResolver, @@ -73,6 +181,15 @@ pub fn load_management_context( }) } +pub fn load_management_context_with_selection( + contract: RadrootsRuntimeManagementContract, + resolver: &RadrootsPathResolver, + selection: &RadrootsRuntimePathSelection, +) -> Result<ManagedRuntimeContext, RadrootsRuntimeManagerError> { + let overrides = selection.caller_overrides()?; + load_management_context(contract, resolver, selection.profile, &overrides) +} + pub fn active_management_mode_for_profile<'a>( contract: &'a RadrootsRuntimeManagementContract, profile: RadrootsPathProfile, @@ -153,6 +270,395 @@ pub fn resolve_runtime_target( } } +pub fn inspect_runtime_status( + target: &ManagedRuntimeTarget, + lifecycle_actions: &[String], +) -> ManagedRuntimeInspection<ManagedRuntimeStatusInspection> { + let availability = if target.runtime_group == ManagedRuntimeGroup::Unknown { + ManagedRuntimeInspectionAvailability::Unconfigured + } else { + ManagedRuntimeInspectionAvailability::Success + }; + + ManagedRuntimeInspection { + availability, + view: ManagedRuntimeStatusInspection { + runtime_id: target.runtime_id.clone(), + instance_id: target.instance_id.clone(), + instance_source: target.instance_source.clone(), + runtime_group: target.runtime_group.as_str().to_owned(), + management_posture: target.runtime_group.posture().to_owned(), + state: status_state(target).to_owned(), + source: "runtime management contract + shared instance registry".to_owned(), + detail: status_detail(target), + management_mode: target.management_mode.clone(), + service_manager_integration: target + .mode_contract + .as_ref() + .map(|mode| mode.service_manager_integration), + uses_absolute_binary_paths: target + .mode_contract + .as_ref() + .map(|mode| mode.uses_absolute_binary_paths), + preferred_cli_binding: target + .bootstrap + .as_ref() + .map(|entry| entry.preferred_cli_binding), + install_state: target + .instance_record + .as_ref() + .map(|record| install_state_label(record.install_state)) + .unwrap_or_else(|| install_state_label(ManagedRuntimeInstallState::NotInstalled)) + .to_owned(), + health_state: infer_health_state(target).0.to_owned(), + health_source: infer_health_state(target).1.to_owned(), + registry_path: target.registry_path.clone(), + lifecycle_actions: if target.runtime_group == ManagedRuntimeGroup::ActiveManagedTarget { + lifecycle_actions.to_vec() + } else { + Vec::new() + }, + instance_paths: target.predicted_paths.clone(), + instance_record: target.instance_record.clone(), + }, + } +} + +pub fn inspect_runtime_logs( + target: &ManagedRuntimeTarget, +) -> ManagedRuntimeInspection<ManagedRuntimeLogsInspection> { + let stdout_log_path = target + .predicted_paths + .as_ref() + .map(|paths| paths.stdout_log_path.clone()); + let stderr_log_path = target + .predicted_paths + .as_ref() + .map(|paths| paths.stderr_log_path.clone()); + let availability = match target.runtime_group { + ManagedRuntimeGroup::Unknown => ManagedRuntimeInspectionAvailability::Unconfigured, + ManagedRuntimeGroup::ActiveManagedTarget => ManagedRuntimeInspectionAvailability::Success, + ManagedRuntimeGroup::DefinedManagedTarget | ManagedRuntimeGroup::BootstrapOnly => { + if target.instance_record.is_some() { + ManagedRuntimeInspectionAvailability::Success + } else { + ManagedRuntimeInspectionAvailability::Unsupported + } + } + }; + let detail = match target.runtime_group { + ManagedRuntimeGroup::ActiveManagedTarget => { + "runtime logs report the managed stdout/stderr locations for the active managed instance" + .to_owned() + } + ManagedRuntimeGroup::DefinedManagedTarget => format!( + "runtime `{}` is only a defined future managed target; no active generic logs surface exists without a registered instance", + target.runtime_id + ), + ManagedRuntimeGroup::BootstrapOnly => format!( + "runtime `{}` remains bootstrap_only and direct-bindable in this wave; generic managed logs are not admitted", + target.runtime_id + ), + ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target), + }; + + ManagedRuntimeInspection { + availability, + view: ManagedRuntimeLogsInspection { + runtime_id: target.runtime_id.clone(), + instance_id: target.instance_id.clone(), + instance_source: target.instance_source.clone(), + runtime_group: target.runtime_group.as_str().to_owned(), + state: match availability { + ManagedRuntimeInspectionAvailability::Success => "ready".to_owned(), + ManagedRuntimeInspectionAvailability::Unconfigured => "unknown_runtime".to_owned(), + ManagedRuntimeInspectionAvailability::Unsupported => "unsupported".to_owned(), + }, + source: "runtime management contract + shared instance registry".to_owned(), + detail, + stdout_log_path: stdout_log_path.clone(), + stderr_log_path: stderr_log_path.clone(), + stdout_log_present: path_present(stdout_log_path.as_ref()).unwrap_or_else(|| { + target + .instance_record + .as_ref() + .is_some_and(|record| record.logs_path.join("stdout.log").exists()) + }), + stderr_log_present: path_present(stderr_log_path.as_ref()).unwrap_or_else(|| { + target + .instance_record + .as_ref() + .is_some_and(|record| record.logs_path.join("stderr.log").exists()) + }), + }, + } +} + +pub fn inspect_runtime_config( + target: &ManagedRuntimeTarget, +) -> ManagedRuntimeInspection<ManagedRuntimeConfigInspection> { + let availability = match target.runtime_group { + ManagedRuntimeGroup::Unknown => ManagedRuntimeInspectionAvailability::Unconfigured, + ManagedRuntimeGroup::ActiveManagedTarget => ManagedRuntimeInspectionAvailability::Success, + ManagedRuntimeGroup::DefinedManagedTarget | ManagedRuntimeGroup::BootstrapOnly => { + if target.instance_record.is_some() { + ManagedRuntimeInspectionAvailability::Success + } else { + ManagedRuntimeInspectionAvailability::Unsupported + } + } + }; + let config_path = target + .instance_record + .as_ref() + .map(|record| record.config_path.clone()); + let detail = match target.runtime_group { + ManagedRuntimeGroup::ActiveManagedTarget => { + if config_path.is_some() { + "runtime config show reports the managed config location without mutating bindings" + .to_owned() + } else { + format!( + "managed runtime `{}` has no registered instance config yet", + target.runtime_id + ) + } + } + ManagedRuntimeGroup::DefinedManagedTarget => format!( + "runtime `{}` is only a defined future managed target; generic config surfaces are not admitted without a registered instance", + target.runtime_id + ), + ManagedRuntimeGroup::BootstrapOnly => format!( + "runtime `{}` remains bootstrap_only and direct-bindable in this wave; generic managed config is not admitted", + target.runtime_id + ), + ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target), + }; + + ManagedRuntimeInspection { + availability, + view: ManagedRuntimeConfigInspection { + runtime_id: target.runtime_id.clone(), + instance_id: target.instance_id.clone(), + instance_source: target.instance_source.clone(), + runtime_group: target.runtime_group.as_str().to_owned(), + state: match availability { + ManagedRuntimeInspectionAvailability::Success => { + if config_path.is_some() { + "ready".to_owned() + } else { + "not_installed".to_owned() + } + } + ManagedRuntimeInspectionAvailability::Unconfigured => "unknown_runtime".to_owned(), + ManagedRuntimeInspectionAvailability::Unsupported => "unsupported".to_owned(), + }, + source: "runtime management contract + shared instance registry".to_owned(), + detail, + config_format: target + .bootstrap + .as_ref() + .map(|entry| entry.config_format.clone()), + config_path: config_path.clone(), + config_present: config_path.as_ref().is_some_and(|path| path.exists()), + requires_bootstrap_secret: target + .bootstrap + .as_ref() + .map(|entry| entry.requires_bootstrap_secret), + requires_config_bootstrap: target + .bootstrap + .as_ref() + .map(|entry| entry.requires_config_bootstrap), + requires_signer_provider: target + .bootstrap + .as_ref() + .map(|entry| entry.requires_signer_provider), + }, + } +} + +pub fn inspect_runtime_action( + target: &ManagedRuntimeTarget, + action: ManagedRuntimeLifecycleAction, + detail_override: Option<String>, +) -> ManagedRuntimeInspection<ManagedRuntimeActionInspection> { + let (availability, state, detail, next_step) = match target.runtime_group { + ManagedRuntimeGroup::ActiveManagedTarget => ( + ManagedRuntimeInspectionAvailability::Unsupported, + "deferred", + detail_override.unwrap_or_else(|| { + format!( + "runtime {} `{}` is not supported for this managed target", + action.as_str().replace('_', " "), + target.runtime_id + ) + }), + None, + ), + ManagedRuntimeGroup::DefinedManagedTarget => ( + ManagedRuntimeInspectionAvailability::Unsupported, + "unsupported", + detail_override.unwrap_or_else(|| { + format!( + "runtime `{}` is only a defined future managed target; `{}` is not admitted in the current wave", + target.runtime_id, + action.as_str().replace('_', " ") + ) + }), + None, + ), + ManagedRuntimeGroup::BootstrapOnly => ( + ManagedRuntimeInspectionAvailability::Unsupported, + "unsupported", + detail_override.unwrap_or_else(|| { + format!( + "runtime `{}` remains bootstrap_only and direct-bindable in this wave; generic managed `{}` is not admitted", + target.runtime_id, + action.as_str().replace('_', " ") + ) + }), + None, + ), + ManagedRuntimeGroup::Unknown => ( + ManagedRuntimeInspectionAvailability::Unconfigured, + "unknown_runtime", + detail_override.unwrap_or_else(|| unknown_runtime_detail(target)), + None, + ), + }; + + ManagedRuntimeInspection { + availability, + view: ManagedRuntimeActionInspection { + action: action.as_str().to_owned(), + runtime_id: target.runtime_id.clone(), + instance_id: target.instance_id.clone(), + instance_source: target.instance_source.clone(), + runtime_group: target.runtime_group.as_str().to_owned(), + state: state.to_owned(), + source: "generic runtime-management command family".to_owned(), + detail, + mutates_bindings: false, + next_step, + }, + } +} + +fn status_state(target: &ManagedRuntimeTarget) -> &'static str { + match target.runtime_group { + ManagedRuntimeGroup::ActiveManagedTarget => match target.instance_record.as_ref() { + Some(record) => install_state_label(record.install_state), + None => "not_installed", + }, + ManagedRuntimeGroup::DefinedManagedTarget => "defined_not_active", + ManagedRuntimeGroup::BootstrapOnly => "bootstrap_only", + ManagedRuntimeGroup::Unknown => "unknown_runtime", + } +} + +fn status_detail(target: &ManagedRuntimeTarget) -> String { + match target.runtime_group { + ManagedRuntimeGroup::ActiveManagedTarget => match &target.instance_record { + Some(record) => format!( + "managed runtime `{}` instance `{}` is registered with config at {}", + target.runtime_id, + target.instance_id, + record.config_path.display() + ), + None => format!( + "managed runtime `{}` has no registered instance `{}` in {}", + target.runtime_id, + target.instance_id, + target.registry_path.display() + ), + }, + ManagedRuntimeGroup::DefinedManagedTarget => format!( + "runtime `{}` is defined in the management contract but not yet admitted as an active managed target", + target.runtime_id + ), + ManagedRuntimeGroup::BootstrapOnly => format!( + "runtime `{}` is bootstrap_only in the management contract and remains direct-bindable outside managed lifecycle in this wave", + target.runtime_id + ), + ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target), + } +} + +fn unknown_runtime_detail(target: &ManagedRuntimeTarget) -> String { + format!( + "runtime `{}` is not present in the current runtime-management contract", + target.runtime_id + ) +} + +fn infer_health_state(target: &ManagedRuntimeTarget) -> (&'static str, &'static str) { + let Some(record) = &target.instance_record else { + return ( + health_state_label(ManagedRuntimeHealthState::NotInstalled), + "registry_absent", + ); + }; + if record.install_state == ManagedRuntimeInstallState::Failed { + return ( + health_state_label(ManagedRuntimeHealthState::Failed), + "registry_install_state", + ); + } + + if let Some(paths) = target.predicted_paths.as_ref() { + if crate::process_running(paths).unwrap_or(false) { + return ( + health_state_label(ManagedRuntimeHealthState::Running), + "process_probe", + ); + } + } else if record.run_path.join("runtime.pid").exists() { + return ( + health_state_label(ManagedRuntimeHealthState::Running), + "pid_file_presence", + ); + } + + match record.install_state { + ManagedRuntimeInstallState::NotInstalled => ( + health_state_label(ManagedRuntimeHealthState::NotInstalled), + "registry_install_state", + ), + ManagedRuntimeInstallState::Installed | ManagedRuntimeInstallState::Configured => ( + health_state_label(ManagedRuntimeHealthState::Stopped), + "pid_file_absent", + ), + ManagedRuntimeInstallState::Failed => ( + health_state_label(ManagedRuntimeHealthState::Failed), + "registry_install_state", + ), + } +} + +fn install_state_label(state: ManagedRuntimeInstallState) -> &'static str { + match state { + ManagedRuntimeInstallState::NotInstalled => "not_installed", + ManagedRuntimeInstallState::Installed => "installed", + ManagedRuntimeInstallState::Configured => "configured", + ManagedRuntimeInstallState::Failed => "failed", + } +} + +fn health_state_label(state: ManagedRuntimeHealthState) -> &'static str { + match state { + ManagedRuntimeHealthState::NotInstalled => "not_installed", + ManagedRuntimeHealthState::Stopped => "stopped", + ManagedRuntimeHealthState::Starting => "starting", + ManagedRuntimeHealthState::Running => "running", + ManagedRuntimeHealthState::Degraded => "degraded", + ManagedRuntimeHealthState::Failed => "failed", + } +} + +fn path_present(path: Option<&PathBuf>) -> Option<bool> { + path.map(|value| value.exists()) +} + pub fn runtime_group( contract: &RadrootsRuntimeManagementContract, runtime_id: &str, @@ -193,10 +699,10 @@ mod tests { }; use super::{ - active_management_mode_for_profile, load_management_context, resolve_runtime_target, - ManagedRuntimeGroup, + ManagedRuntimeGroup, active_management_mode_for_profile, load_management_context, + resolve_runtime_target, }; - use crate::{parse_contract_str, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord}; + use crate::{ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, parse_contract_str}; const CONTRACT: &str = r#" schema = "radroots-runtime-management" diff --git a/crates/runtime_paths/Cargo.toml b/crates/runtime_paths/Cargo.toml @@ -13,4 +13,5 @@ documentation = "https://docs.rs/radroots_runtime_paths" readme = "README" [dependencies] +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } diff --git a/crates/runtime_paths/src/lib.rs b/crates/runtime_paths/src/lib.rs @@ -9,19 +9,25 @@ pub mod roots; pub mod service; pub use conventions::{ - 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, + 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, }; pub use error::RadrootsRuntimePathsError; pub use migration::{ - inspect_legacy_paths, RadrootsLegacyPathCandidate, RadrootsLegacyPathDetection, - RadrootsMigrationReport, RADROOTS_MIGRATION_COMPATIBILITY_WINDOW, RADROOTS_MIGRATION_POSTURE, + RADROOTS_MIGRATION_COMPATIBILITY_WINDOW, RADROOTS_MIGRATION_POSTURE, + RadrootsLegacyPathCandidate, RadrootsLegacyPathDetection, RadrootsMigrationReport, + inspect_legacy_paths, }; pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind}; pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform}; pub use roots::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths}; -pub use service::{RadrootsRuntimePathSelection, RadrootsRuntimePathSelectionError}; +pub use service::{ + RadrootsRuntimeLegacyPathContract, RadrootsRuntimeMigrationContract, + RadrootsRuntimePathPolicyContract, RadrootsRuntimePathSelection, + RadrootsRuntimePathSelectionError, RadrootsRuntimeSelectionContract, + RadrootsRuntimeSelectionOverrideContract, runtime_migration_contract, +}; #[cfg(test)] mod tests { diff --git a/crates/runtime_paths/src/service.rs b/crates/runtime_paths/src/service.rs @@ -1,10 +1,11 @@ use std::path::PathBuf; +use serde::Serialize; use thiserror::Error; use crate::{ - RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths, - RadrootsRuntimeNamespace, RadrootsRuntimePathsError, + RadrootsMigrationReport, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPaths, RadrootsRuntimeNamespace, RadrootsRuntimePathsError, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -15,6 +16,93 @@ pub struct RadrootsRuntimePathSelection { pub repo_local_root_source: Option<String>, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct RadrootsRuntimeSelectionContract { + pub active_profile: String, + pub allowed_profiles: Vec<String>, + pub path_overrides: RadrootsRuntimeSelectionOverrideContract, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct RadrootsRuntimeSelectionOverrideContract { + pub profile_source: String, + pub root_source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub repo_local_root: Option<PathBuf>, + #[serde(skip_serializing_if = "Option::is_none")] + pub repo_local_root_source: Option<String>, + pub subordinate_path_override_source: String, + pub subordinate_path_override_keys: Vec<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct RadrootsRuntimePathPolicyContract { + pub canonical_root_selection: String, + pub canonical_subordinate_path_override: String, + pub leaf_path_env_posture: String, + pub compatibility_leaf_path_keys: Vec<String>, +} + +impl RadrootsRuntimePathPolicyContract { + pub fn new( + canonical_root_selection: &str, + canonical_subordinate_path_override: &str, + leaf_path_env_posture: &str, + compatibility_leaf_path_keys: &[&str], + ) -> Self { + Self { + canonical_root_selection: canonical_root_selection.to_owned(), + canonical_subordinate_path_override: canonical_subordinate_path_override.to_owned(), + leaf_path_env_posture: leaf_path_env_posture.to_owned(), + compatibility_leaf_path_keys: compatibility_leaf_path_keys + .iter() + .map(|entry| (*entry).to_owned()) + .collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct RadrootsRuntimeMigrationContract { + pub posture: String, + pub state: String, + pub silent_startup_relocation: bool, + pub compatibility_window: String, + pub detected_legacy_paths: Vec<RadrootsRuntimeLegacyPathContract>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct RadrootsRuntimeLegacyPathContract { + pub id: String, + pub description: String, + pub path: PathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + pub destination: Option<PathBuf>, + pub import_hint: String, +} + +pub fn runtime_migration_contract( + report: RadrootsMigrationReport, +) -> RadrootsRuntimeMigrationContract { + RadrootsRuntimeMigrationContract { + posture: report.posture.to_owned(), + state: report.state.to_owned(), + silent_startup_relocation: report.silent_startup_relocation, + compatibility_window: report.compatibility_window.to_owned(), + detected_legacy_paths: report + .detected_legacy_paths + .into_iter() + .map(|path| RadrootsRuntimeLegacyPathContract { + id: path.id, + description: path.description, + path: path.path, + destination: path.destination, + import_hint: path.import_hint, + }) + .collect(), + } +} + #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum RadrootsRuntimePathSelectionError { #[error("{env_var} must be valid utf-8 when set")] @@ -25,6 +113,11 @@ pub enum RadrootsRuntimePathSelectionError { )] InvalidProfileEnv { env_var: String, value: String }, + #[error( + "profile must be `interactive_user`, `service_host`, `repo_local`, or `mobile_native`; found `{value}`" + )] + InvalidProfileValue { value: String }, + #[error("{repo_local_root_env} must be set when {profile_env}=repo_local")] MissingRepoLocalRoot { profile_env: String, @@ -45,6 +138,13 @@ impl RadrootsRuntimePathSelection { } } + pub fn from_profile_value( + profile: &str, + repo_local_root: Option<PathBuf>, + ) -> Result<Self, RadrootsRuntimePathSelectionError> { + Ok(Self::caller(parse_profile_value(profile)?, repo_local_root)) + } + pub fn from_env( profile_env: &'static str, repo_local_root_env: &'static str, @@ -88,12 +188,52 @@ impl RadrootsRuntimePathSelection { profile_env: &'static str, repo_local_root_env: &'static str, ) -> Result<RadrootsPathOverrides, RadrootsRuntimePathSelectionError> { + self.overrides_with_labels(profile_env, repo_local_root_env) + } + + pub fn caller_overrides( + &self, + ) -> Result<RadrootsPathOverrides, RadrootsRuntimePathSelectionError> { + self.overrides_with_labels("caller_profile", "caller_repo_local_root") + } + + pub fn contract( + &self, + allowed_profiles: &[&str], + subordinate_path_override_source: &str, + subordinate_path_override_keys: &[&str], + ) -> RadrootsRuntimeSelectionContract { + RadrootsRuntimeSelectionContract { + active_profile: self.profile.to_string(), + allowed_profiles: allowed_profiles + .iter() + .map(|entry| (*entry).to_owned()) + .collect(), + path_overrides: RadrootsRuntimeSelectionOverrideContract { + profile_source: self.profile_source.clone(), + root_source: self.root_source().to_owned(), + repo_local_root: self.repo_local_root.clone(), + repo_local_root_source: self.repo_local_root_source.clone(), + subordinate_path_override_source: subordinate_path_override_source.to_owned(), + subordinate_path_override_keys: subordinate_path_override_keys + .iter() + .map(|entry| (*entry).to_owned()) + .collect(), + }, + } + } + + fn overrides_with_labels( + &self, + profile_label: &str, + repo_local_root_label: &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(), + profile_env: profile_label.to_owned(), + repo_local_root_env: repo_local_root_label.to_owned(), }); }; Ok(RadrootsPathOverrides::repo_local(repo_local_root)) @@ -120,12 +260,27 @@ fn parse_profile( env_var: &'static str, value: &str, ) -> Result<RadrootsPathProfile, RadrootsRuntimePathSelectionError> { + match parse_profile_value(value) { + Ok(profile) => Ok(profile), + Err(RadrootsRuntimePathSelectionError::InvalidProfileValue { value }) => { + Err(RadrootsRuntimePathSelectionError::InvalidProfileEnv { + env_var: env_var.to_owned(), + value, + }) + } + Err(other) => Err(other), + } +} + +fn parse_profile_value( + 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(), + "mobile_native" => Ok(RadrootsPathProfile::MobileNative), + other => Err(RadrootsRuntimePathSelectionError::InvalidProfileValue { value: other.to_owned(), }), } @@ -139,7 +294,11 @@ mod tests { RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, }; - use super::{RadrootsRuntimePathSelection, RadrootsRuntimePathSelectionError}; + use super::{ + RadrootsRuntimePathPolicyContract, RadrootsRuntimePathSelection, + RadrootsRuntimePathSelectionError, runtime_migration_contract, + }; + use crate::{RadrootsLegacyPathDetection, RadrootsMigrationReport}; #[test] fn caller_selection_preserves_profile_and_sources() { @@ -214,4 +373,80 @@ mod tests { } ); } + + #[test] + fn profile_value_selection_accepts_mobile_native() { + let selection = RadrootsRuntimePathSelection::from_profile_value("mobile_native", None) + .expect("mobile native profile"); + + assert_eq!(selection.profile, RadrootsPathProfile::MobileNative); + assert_eq!(selection.profile_source, "caller"); + } + + #[test] + fn contract_captures_selection_sources() { + let selection = RadrootsRuntimePathSelection::caller( + RadrootsPathProfile::RepoLocal, + Some(PathBuf::from("/repo/.local/radroots")), + ); + + let contract = selection.contract( + &["interactive_user", "repo_local"], + "config_artifact", + &["config.service.logs_dir"], + ); + + assert_eq!(contract.active_profile, "repo_local"); + assert_eq!( + contract.allowed_profiles, + vec!["interactive_user".to_owned(), "repo_local".to_owned()] + ); + assert_eq!(contract.path_overrides.profile_source, "caller"); + assert_eq!( + contract.path_overrides.repo_local_root, + Some(PathBuf::from("/repo/.local/radroots")) + ); + } + + #[test] + fn path_policy_contract_preserves_policy_strings() { + let contract = RadrootsRuntimePathPolicyContract::new( + "profile_root_env_or_repo_wrapper", + "config_artifact", + "compatibility_break_glass", + &["MYC_PATHS_STATE_DIR"], + ); + + assert_eq!( + contract.canonical_root_selection, + "profile_root_env_or_repo_wrapper" + ); + assert_eq!( + contract.compatibility_leaf_path_keys, + vec!["MYC_PATHS_STATE_DIR".to_owned()] + ); + } + + #[test] + fn runtime_migration_contract_maps_detected_paths() { + let report = RadrootsMigrationReport { + posture: "explicit_operator_import_required", + state: "legacy_state_detected", + silent_startup_relocation: false, + compatibility_window: "detect_and_report_only", + detected_legacy_paths: vec![RadrootsLegacyPathDetection { + id: "legacy_path".to_owned(), + description: "legacy path".to_owned(), + path: PathBuf::from("/tmp/legacy"), + destination: Some(PathBuf::from("/tmp/new")), + import_hint: "copy it manually".to_owned(), + }], + }; + + let contract = runtime_migration_contract(report); + + assert_eq!(contract.posture, "explicit_operator_import_required"); + assert_eq!(contract.detected_legacy_paths.len(), 1); + assert_eq!(contract.detected_legacy_paths[0].id, "legacy_path"); + } }