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:
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");
+ }
}