commit 688543521e41a742df9d7152cdeda107f08d2903
parent 886ff6b38763019f12cb7a81e3cbb654e0d2710a
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Apr 2026 23:54:44 +0000
manager: add shared runtime control boundary
- add the runtime-manager crate with typed management contract parsing
- resolve shared and per-instance runtime control paths from runtime-paths
- persist managed runtime instance registry records and bootstrap lookups
- wire the crate into workspace coverage and release metadata
Diffstat:
11 files changed, 804 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2628,6 +2628,17 @@ dependencies = [
]
[[package]]
+name = "radroots-runtime-manager"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "radroots-runtime-paths",
+ "serde",
+ "tempfile",
+ "thiserror 1.0.69",
+ "toml",
+]
+
+[[package]]
name = "radroots-runtime-paths"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -37,6 +37,7 @@ members = [
"crates/replica-db-wasm",
"crates/runtime-paths",
"crates/runtime-distribution",
+ "crates/runtime-manager",
"crates/trade",
"crates/types",
"crates/protected-store",
@@ -68,6 +69,7 @@ radroots-nostr-ndb = { path = "crates/nostr-ndb", version = "0.1.0-alpha.1", def
radroots-runtime = { path = "crates/runtime", version = "0.1.0-alpha.1", default-features = false }
radroots-runtime-paths = { path = "crates/runtime-paths", version = "0.1.0-alpha.1", default-features = false }
radroots-runtime-distribution = { path = "crates/runtime-distribution", version = "0.1.0-alpha.1", default-features = false }
+radroots-runtime-manager = { path = "crates/runtime-manager", version = "0.1.0-alpha.1", default-features = false }
radroots-log = { path = "crates/log", version = "0.1.0-alpha.1", default-features = false }
radroots-net = { path = "crates/net", version = "0.1.0-alpha.1", default-features = false }
radroots-net-core = { path = "crates/net-core", version = "0.1.0-alpha.1", default-features = false }
diff --git a/contract/coverage/policy.toml b/contract/coverage/policy.toml
@@ -31,6 +31,7 @@ crates = [
"radroots-runtime",
"radroots-runtime-paths",
"radroots-runtime-distribution",
+ "radroots-runtime-manager",
"radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml
@@ -11,6 +11,7 @@ crates = [
"radroots-runtime",
"radroots-runtime-paths",
"radroots-runtime-distribution",
+ "radroots-runtime-manager",
"radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
@@ -51,6 +52,7 @@ crates = [
"radroots-runtime",
"radroots-runtime-paths",
"radroots-runtime-distribution",
+ "radroots-runtime-manager",
"radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
diff --git a/crates/runtime-manager/Cargo.toml b/crates/runtime-manager/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "radroots-runtime-manager"
+version = "0.1.0-alpha.1"
+edition.workspace = true
+authors = [
+ "Radroots Authors",
+]
+rust-version.workspace = true
+license.workspace = true
+description = "shared local managed-runtime paths, registry, and lifecycle boundary for radroots"
+repository.workspace = true
+homepage.workspace = true
+documentation = "https://docs.rs/radroots-runtime-manager"
+readme = "README.md"
+
+[dependencies]
+radroots-runtime-paths = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+thiserror = { workspace = true }
+toml = { workspace = true }
+
+[dev-dependencies]
+tempfile = { workspace = true }
diff --git a/crates/runtime-manager/README.md b/crates/runtime-manager/README.md
@@ -0,0 +1,14 @@
+# radroots-runtime-manager
+
+Shared local managed-runtime paths, registry, and lifecycle boundary for Rad Roots runtimes.
+
+## Goals
+
+- parse the machine-readable runtime management contract
+- resolve shared manager roots and per-instance runtime-manager paths
+- persist one shared managed-runtime instance registry
+- define typed install and health state surfaces for CLI and app adoption later
+
+## License
+
+Licensed under AGPL-3.0. See LICENSE.
diff --git a/crates/runtime-manager/src/error.rs b/crates/runtime-manager/src/error.rs
@@ -0,0 +1,46 @@
+use std::path::PathBuf;
+
+use radroots_runtime_paths::RadrootsRuntimePathsError;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum RadrootsRuntimeManagerError {
+ #[error("parse runtime management contract: {0}")]
+ Parse(String),
+ #[error("runtime management schema `{found}` does not match `{expected}`")]
+ UnexpectedSchema {
+ expected: &'static str,
+ found: String,
+ },
+ #[error("management mode `{0}` not found in runtime management contract")]
+ UnknownManagementMode(String),
+ #[error("management mode `{mode_id}` does not support profile `{profile}`")]
+ UnsupportedProfile { mode_id: String, profile: String },
+ #[error("management mode `{0}` has no shared path specification")]
+ MissingPathSpec(String),
+ #[error("unknown root class `{0}` in runtime management contract")]
+ UnknownRootClass(String),
+ #[error("runtime `{0}` has no bootstrap entry in runtime management contract")]
+ UnknownBootstrapRuntime(String),
+ #[error("read runtime instance registry {path}: {source}")]
+ ReadRegistry {
+ path: PathBuf,
+ source: std::io::Error,
+ },
+ #[error("parse runtime instance registry {path}: {details}")]
+ ParseRegistry { path: PathBuf, details: String },
+ #[error("serialize runtime instance registry: {0}")]
+ SerializeRegistry(String),
+ #[error("create runtime instance registry parent {path}: {source}")]
+ CreateRegistryParent {
+ path: PathBuf,
+ source: std::io::Error,
+ },
+ #[error("write runtime instance registry {path}: {source}")]
+ WriteRegistry {
+ path: PathBuf,
+ source: std::io::Error,
+ },
+ #[error(transparent)]
+ RuntimePaths(#[from] RadrootsRuntimePathsError),
+}
diff --git a/crates/runtime-manager/src/lib.rs b/crates/runtime-manager/src/lib.rs
@@ -0,0 +1,305 @@
+#![forbid(unsafe_code)]
+
+pub mod error;
+pub mod model;
+pub mod paths;
+pub mod registry;
+
+pub use error::RadrootsRuntimeManagerError;
+pub use model::{
+ BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState,
+ ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry,
+ ManagementDefaults, ManagementModeContract, ManagementPathContract,
+ RadrootsRuntimeManagementContract, RuntimeGroups,
+};
+pub use paths::{
+ ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime,
+ resolve_instance_paths, resolve_shared_paths,
+};
+pub use registry::{instance, load_registry, save_registry, upsert_instance};
+
+pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management";
+
+pub fn parse_contract_str(
+ raw: &str,
+) -> Result<RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError> {
+ let contract = toml::from_str::<RadrootsRuntimeManagementContract>(raw)
+ .map_err(|err| RadrootsRuntimeManagerError::Parse(err.to_string()))?;
+ if contract.schema != RUNTIME_MANAGEMENT_SCHEMA {
+ return Err(RadrootsRuntimeManagerError::UnexpectedSchema {
+ expected: RUNTIME_MANAGEMENT_SCHEMA,
+ found: contract.schema.clone(),
+ });
+ }
+ Ok(contract)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform,
+ };
+ 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,
+ };
+
+ 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 = ["community-app-desktop"]
+
+[managed_runtime_targets]
+active = ["radrootsd"]
+defined = ["myc", "rhi"]
+bootstrap_only = ["hyf"]
+
+[lifecycle]
+actions = ["install", "uninstall", "start", "stop", "restart", "status", "logs", "config_show", "config_set"]
+destructive_actions = ["uninstall"]
+health_states = ["not_installed", "stopped", "starting", "running", "degraded", "failed"]
+
+[mode.interactive_user_managed]
+contract_state = "active"
+platforms = ["linux", "macos", "windows"]
+supported_profiles = ["interactive_user", "repo_local"]
+service_manager_integration = false
+uses_absolute_binary_paths = true
+requires_explicit_pid_tracking = true
+requires_explicit_log_tracking = true
+default_instance_cardinality = "single_default_instance"
+
+[mode.service_host_managed]
+contract_state = "defined"
+platforms = ["linux", "macos", "windows"]
+supported_profiles = ["service_host"]
+service_manager_integration = true
+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",
+ "instance_id",
+ "management_mode",
+ "install_state",
+ "binary_path",
+ "config_path",
+ "logs_path",
+ "run_path",
+ "installed_version",
+]
+optional_fields = [
+ "health_endpoint",
+ "secret_material_ref",
+ "last_started_at",
+ "last_stopped_at",
+ "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 parse_contract_accepts_expected_schema() {
+ let contract = parse_contract_str(CONTRACT).expect("parse contract");
+ assert_eq!(contract.schema, crate::RUNTIME_MANAGEMENT_SCHEMA);
+ assert!(contract.mode.contains_key("interactive_user_managed"));
+ }
+
+ #[test]
+ fn resolve_shared_paths_uses_interactive_user_roots() {
+ let contract = parse_contract_str(CONTRACT).expect("parse contract");
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+
+ let paths = resolve_shared_paths(
+ &contract,
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ "interactive_user_managed",
+ )
+ .expect("resolve shared manager paths");
+
+ assert_eq!(
+ paths.instance_registry_path,
+ PathBuf::from("/home/treesap/.radroots/config/shared/runtime-manager/instances.toml")
+ );
+ assert_eq!(
+ paths.install_root,
+ PathBuf::from("/home/treesap/.radroots/data/shared/runtime-manager/installs")
+ );
+ assert_eq!(
+ paths.logs_root,
+ PathBuf::from("/home/treesap/.radroots/logs/shared/runtime-manager")
+ );
+ }
+
+ #[test]
+ fn resolve_repo_local_paths_uses_explicit_base_root() {
+ let contract = parse_contract_str(CONTRACT).expect("parse contract");
+ let resolver =
+ RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
+
+ let paths = resolve_shared_paths(
+ &contract,
+ &resolver,
+ RadrootsPathProfile::RepoLocal,
+ &RadrootsPathOverrides::repo_local("/repo/.local/radroots"),
+ "interactive_user_managed",
+ )
+ .expect("resolve repo local manager paths");
+
+ assert_eq!(
+ paths.state_root,
+ PathBuf::from("/repo/.local/radroots/data/shared/runtime-manager/state")
+ );
+ }
+
+ #[test]
+ fn resolve_instance_paths_builds_per_runtime_layout() {
+ let contract = parse_contract_str(CONTRACT).expect("parse contract");
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Macos,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/Users/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+ let shared = resolve_shared_paths(
+ &contract,
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ "interactive_user_managed",
+ )
+ .expect("resolve shared manager paths");
+
+ let instance_paths = resolve_instance_paths(&shared, "radrootsd", "local");
+ assert_eq!(
+ instance_paths.install_dir,
+ PathBuf::from(
+ "/Users/treesap/.radroots/data/shared/runtime-manager/installs/radrootsd/local"
+ )
+ );
+ assert_eq!(
+ instance_paths.pid_file_path,
+ PathBuf::from(
+ "/Users/treesap/.radroots/run/shared/runtime-manager/radrootsd/local/runtime.pid"
+ )
+ );
+ assert_eq!(
+ instance_paths.metadata_path,
+ PathBuf::from(
+ "/Users/treesap/.radroots/data/shared/runtime-manager/state/radrootsd/local/instance.toml"
+ )
+ );
+ }
+
+ #[test]
+ fn registry_round_trip_persists_and_reloads_instances() {
+ let dir = tempdir().expect("tempdir");
+ let registry_path = dir.path().join("instances.toml");
+ let mut registry = crate::ManagedRuntimeInstanceRegistry::default();
+ upsert_instance(
+ &mut registry,
+ ManagedRuntimeInstanceRecord {
+ runtime_id: "radrootsd".to_string(),
+ instance_id: "local".to_string(),
+ management_mode: "interactive_user_managed".to_string(),
+ install_state: ManagedRuntimeInstallState::Configured,
+ binary_path: PathBuf::from("/tmp/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.1".to_string(),
+ health_endpoint: Some("jsonrpc_status".to_string()),
+ secret_material_ref: Some(
+ "shared/runtime-manager/radrootsd/local/token".to_string(),
+ ),
+ last_started_at: Some("2026-04-08T00:00:00Z".to_string()),
+ last_stopped_at: None,
+ notes: Some("test".to_string()),
+ },
+ );
+
+ save_registry(®istry_path, ®istry).expect("save registry");
+ let reloaded = load_registry(®istry_path).expect("load registry");
+ let record = instance(&reloaded, "radrootsd", "local").expect("instance record");
+ assert_eq!(record.install_state, ManagedRuntimeInstallState::Configured);
+ assert_eq!(record.health_endpoint.as_deref(), Some("jsonrpc_status"));
+ }
+
+ #[test]
+ fn bootstrap_lookup_returns_radrootsd_contract() {
+ let contract = parse_contract_str(CONTRACT).expect("parse contract");
+ let bootstrap = bootstrap_runtime(&contract, "radrootsd").expect("bootstrap contract");
+ assert_eq!(bootstrap.default_instance_id, "local");
+ assert_eq!(bootstrap.health_surface, "jsonrpc_status");
+ assert!(bootstrap.preferred_cli_binding);
+ }
+
+ #[test]
+ fn install_and_health_state_surface_is_typed() {
+ assert_eq!(
+ ManagedRuntimeInstallState::Installed,
+ ManagedRuntimeInstallState::Installed
+ );
+ assert_eq!(
+ ManagedRuntimeHealthState::Degraded,
+ ManagedRuntimeHealthState::Degraded
+ );
+ }
+}
diff --git a/crates/runtime-manager/src/model.rs b/crates/runtime-manager/src/model.rs
@@ -0,0 +1,162 @@
+use std::collections::BTreeMap;
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct RadrootsRuntimeManagementContract {
+ pub schema: String,
+ pub schema_version: u32,
+ pub owner_doc: String,
+ pub runtime_registry: String,
+ pub distribution_contract: String,
+ pub capabilities_contract: String,
+ pub defaults: ManagementDefaults,
+ pub management_clients: RuntimeGroups,
+ pub managed_runtime_targets: RuntimeGroups,
+ 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)]
+pub struct ManagementDefaults {
+ pub instance_cardinality: String,
+ pub managed_runtime_lookup: String,
+ pub explicit_runtime_endpoint_overrides_precede_managed_instance_binding: bool,
+ pub global_path_mutation_forbidden: bool,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
+pub struct RuntimeGroups {
+ #[serde(default)]
+ pub active: Vec<String>,
+ #[serde(default)]
+ pub defined: Vec<String>,
+ #[serde(default)]
+ pub bootstrap_only: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct LifecycleContract {
+ #[serde(default)]
+ pub actions: Vec<String>,
+ #[serde(default)]
+ pub destructive_actions: Vec<String>,
+ #[serde(default)]
+ pub health_states: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct ManagementModeContract {
+ pub contract_state: String,
+ #[serde(default)]
+ pub platforms: Vec<String>,
+ #[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)]
+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)]
+pub struct InstanceMetadataContract {
+ #[serde(default)]
+ pub required_fields: Vec<String>,
+ #[serde(default)]
+ pub optional_fields: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct BootstrapRuntimeContract {
+ pub runtime_id: String,
+ pub management_mode: String,
+ pub default_instance_id: String,
+ pub install_strategy: String,
+ pub config_format: String,
+ pub requires_bootstrap_secret: bool,
+ pub requires_config_bootstrap: bool,
+ pub requires_signer_provider: bool,
+ pub health_surface: String,
+ pub preferred_cli_binding: bool,
+ pub notes: Option<String>,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ManagedRuntimeInstallState {
+ NotInstalled,
+ Installed,
+ Configured,
+ Failed,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ManagedRuntimeHealthState {
+ NotInstalled,
+ Stopped,
+ Starting,
+ Running,
+ Degraded,
+ Failed,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct ManagedRuntimeInstanceRecord {
+ pub runtime_id: String,
+ pub instance_id: String,
+ pub management_mode: String,
+ pub install_state: ManagedRuntimeInstallState,
+ pub binary_path: PathBuf,
+ pub config_path: PathBuf,
+ pub logs_path: PathBuf,
+ pub run_path: PathBuf,
+ pub installed_version: String,
+ pub health_endpoint: Option<String>,
+ pub secret_material_ref: Option<String>,
+ pub last_started_at: Option<String>,
+ pub last_stopped_at: Option<String>,
+ pub notes: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct ManagedRuntimeInstanceRegistry {
+ pub schema: String,
+ pub schema_version: u32,
+ #[serde(default)]
+ pub instances: Vec<ManagedRuntimeInstanceRecord>,
+}
+
+impl Default for ManagedRuntimeInstanceRegistry {
+ fn default() -> Self {
+ Self {
+ schema: "radroots-runtime-instance-registry".to_string(),
+ schema_version: 1,
+ instances: Vec::new(),
+ }
+ }
+}
diff --git a/crates/runtime-manager/src/paths.rs b/crates/runtime-manager/src/paths.rs
@@ -0,0 +1,157 @@
+use std::path::PathBuf;
+
+use radroots_runtime_paths::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths,
+};
+
+use crate::error::RadrootsRuntimeManagerError;
+use crate::model::RadrootsRuntimeManagementContract;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ManagedRuntimeSharedPaths {
+ pub instance_registry_path: PathBuf,
+ pub artifact_cache_dir: PathBuf,
+ pub install_root: PathBuf,
+ pub state_root: PathBuf,
+ pub logs_root: PathBuf,
+ pub run_root: PathBuf,
+ pub secrets_root: PathBuf,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ManagedRuntimeInstancePaths {
+ pub install_dir: PathBuf,
+ pub state_dir: PathBuf,
+ pub logs_dir: PathBuf,
+ pub run_dir: PathBuf,
+ pub secrets_dir: PathBuf,
+ pub pid_file_path: PathBuf,
+ pub stdout_log_path: PathBuf,
+ pub stderr_log_path: PathBuf,
+ pub metadata_path: PathBuf,
+}
+
+pub fn resolve_shared_paths(
+ contract: &RadrootsRuntimeManagementContract,
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+ mode_id: &str,
+) -> Result<ManagedRuntimeSharedPaths, RadrootsRuntimeManagerError> {
+ ensure_profile_supported(contract, mode_id, profile)?;
+ let roots = resolver.resolve(profile, overrides)?;
+ let path_spec = contract
+ .paths
+ .get(mode_id)
+ .ok_or_else(|| RadrootsRuntimeManagerError::MissingPathSpec(mode_id.to_string()))?;
+
+ Ok(ManagedRuntimeSharedPaths {
+ instance_registry_path: root_class_path(
+ &roots,
+ &path_spec.instance_registry_root_class,
+ &path_spec.instance_registry_rel,
+ )?,
+ artifact_cache_dir: root_class_path(
+ &roots,
+ &path_spec.artifact_cache_root_class,
+ &path_spec.artifact_cache_rel,
+ )?,
+ install_root: root_class_path(
+ &roots,
+ &path_spec.install_root_class,
+ &path_spec.install_root_rel,
+ )?,
+ state_root: root_class_path(
+ &roots,
+ &path_spec.state_root_class,
+ &path_spec.state_root_rel,
+ )?,
+ logs_root: root_class_path(&roots, &path_spec.logs_root_class, &path_spec.logs_root_rel)?,
+ run_root: root_class_path(&roots, &path_spec.run_root_class, &path_spec.run_root_rel)?,
+ secrets_root: root_class_path(
+ &roots,
+ &path_spec.secrets_root_class,
+ &path_spec.secrets_namespace_rel,
+ )?,
+ })
+}
+
+pub fn resolve_instance_paths(
+ shared: &ManagedRuntimeSharedPaths,
+ runtime_id: &str,
+ instance_id: &str,
+) -> ManagedRuntimeInstancePaths {
+ let suffix = PathBuf::from(runtime_id).join(instance_id);
+ let install_dir = shared.install_root.join(&suffix);
+ let state_dir = shared.state_root.join(&suffix);
+ let logs_dir = shared.logs_root.join(&suffix);
+ let run_dir = shared.run_root.join(&suffix);
+ let secrets_dir = shared.secrets_root.join(&suffix);
+
+ ManagedRuntimeInstancePaths {
+ install_dir,
+ state_dir: state_dir.clone(),
+ logs_dir: logs_dir.clone(),
+ run_dir: run_dir.clone(),
+ secrets_dir,
+ pid_file_path: run_dir.join("runtime.pid"),
+ stdout_log_path: logs_dir.join("stdout.log"),
+ stderr_log_path: logs_dir.join("stderr.log"),
+ metadata_path: state_dir.join("instance.toml"),
+ }
+}
+
+pub fn bootstrap_runtime<'a>(
+ contract: &'a RadrootsRuntimeManagementContract,
+ runtime_id: &str,
+) -> Result<&'a crate::model::BootstrapRuntimeContract, RadrootsRuntimeManagerError> {
+ contract
+ .bootstrap
+ .get(runtime_id)
+ .ok_or_else(|| RadrootsRuntimeManagerError::UnknownBootstrapRuntime(runtime_id.to_string()))
+}
+
+fn ensure_profile_supported(
+ contract: &RadrootsRuntimeManagementContract,
+ mode_id: &str,
+ profile: RadrootsPathProfile,
+) -> Result<(), RadrootsRuntimeManagerError> {
+ let mode = contract
+ .mode
+ .get(mode_id)
+ .ok_or_else(|| RadrootsRuntimeManagerError::UnknownManagementMode(mode_id.to_string()))?;
+ let profile_id = profile.to_string();
+ if mode
+ .supported_profiles
+ .iter()
+ .any(|entry| entry == &profile_id)
+ {
+ Ok(())
+ } else {
+ Err(RadrootsRuntimeManagerError::UnsupportedProfile {
+ mode_id: mode_id.to_string(),
+ profile: profile_id,
+ })
+ }
+}
+
+fn root_class_path(
+ roots: &RadrootsPaths,
+ root_class: &str,
+ rel: &str,
+) -> Result<PathBuf, RadrootsRuntimeManagerError> {
+ let base = match root_class {
+ "config" => &roots.config,
+ "data" => &roots.data,
+ "cache" => &roots.cache,
+ "logs" => &roots.logs,
+ "run" => &roots.run,
+ "secrets" => &roots.secrets,
+ other => {
+ return Err(RadrootsRuntimeManagerError::UnknownRootClass(
+ other.to_string(),
+ ));
+ }
+ };
+ Ok(base.join(rel))
+}
diff --git a/crates/runtime-manager/src/registry.rs b/crates/runtime-manager/src/registry.rs
@@ -0,0 +1,81 @@
+use std::fs;
+use std::path::Path;
+
+use crate::error::RadrootsRuntimeManagerError;
+use crate::model::{ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry};
+
+pub fn load_registry(
+ path: impl AsRef<Path>,
+) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> {
+ let path = path.as_ref();
+ let raw = match fs::read_to_string(path) {
+ Ok(raw) => raw,
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
+ return Ok(ManagedRuntimeInstanceRegistry::default());
+ }
+ Err(source) => {
+ return Err(RadrootsRuntimeManagerError::ReadRegistry {
+ path: path.to_path_buf(),
+ source,
+ });
+ }
+ };
+
+ toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw).map_err(|source| {
+ RadrootsRuntimeManagerError::ParseRegistry {
+ path: path.to_path_buf(),
+ details: source.to_string(),
+ }
+ })
+}
+
+pub fn save_registry(
+ path: impl AsRef<Path>,
+ registry: &ManagedRuntimeInstanceRegistry,
+) -> Result<(), RadrootsRuntimeManagerError> {
+ let path = path.as_ref();
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent).map_err(|source| {
+ RadrootsRuntimeManagerError::CreateRegistryParent {
+ path: parent.to_path_buf(),
+ source,
+ }
+ })?;
+ }
+
+ let raw = toml::to_string_pretty(registry)
+ .map_err(|err| RadrootsRuntimeManagerError::SerializeRegistry(err.to_string()))?;
+ fs::write(path, raw).map_err(|source| RadrootsRuntimeManagerError::WriteRegistry {
+ path: path.to_path_buf(),
+ source,
+ })
+}
+
+pub fn upsert_instance(
+ registry: &mut ManagedRuntimeInstanceRegistry,
+ record: ManagedRuntimeInstanceRecord,
+) {
+ if let Some(existing) = registry.instances.iter_mut().find(|existing| {
+ existing.runtime_id == record.runtime_id && existing.instance_id == record.instance_id
+ }) {
+ *existing = record;
+ } else {
+ registry.instances.push(record);
+ registry.instances.sort_by(|left, right| {
+ left.runtime_id
+ .cmp(&right.runtime_id)
+ .then_with(|| left.instance_id.cmp(&right.instance_id))
+ });
+ }
+}
+
+pub fn instance<'a>(
+ registry: &'a ManagedRuntimeInstanceRegistry,
+ runtime_id: &str,
+ instance_id: &str,
+) -> Option<&'a ManagedRuntimeInstanceRecord> {
+ registry
+ .instances
+ .iter()
+ .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)
+}