lib.rs (13184B)
1 #![forbid(unsafe_code)] 2 3 pub mod error; 4 pub mod lifecycle; 5 pub mod managed; 6 pub mod model; 7 pub mod paths; 8 pub mod registry; 9 10 pub use error::RadrootsRuntimeManagerError; 11 pub use lifecycle::{ 12 ensure_instance_layout, extract_binary_archive, install_binary, process_running, 13 read_secret_file, remove_instance_artifacts, start_process, stop_process, 14 write_instance_metadata, write_managed_file, write_secret_file, 15 }; 16 pub use managed::{ 17 ManagedRuntimeActionInspection, ManagedRuntimeConfigInspection, ManagedRuntimeContext, 18 ManagedRuntimeGroup, ManagedRuntimeInspection, ManagedRuntimeInspectionAvailability, 19 ManagedRuntimeLifecycleAction, ManagedRuntimeLogsInspection, ManagedRuntimeStatusInspection, 20 ManagedRuntimeTarget, active_management_mode_for_profile, inspect_runtime_action, 21 inspect_runtime_config, inspect_runtime_logs, inspect_runtime_status, load_management_context, 22 load_management_context_with_selection, resolve_runtime_target, runtime_group, 23 }; 24 pub use model::{ 25 BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState, 26 ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, 27 ManagementDefaults, ManagementModeContract, ManagementPathContract, 28 RadrootsRuntimeManagementContract, RuntimeGroups, 29 }; 30 pub use paths::{ 31 ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime, 32 resolve_instance_paths, resolve_shared_paths, 33 }; 34 pub use registry::{instance, load_registry, remove_instance, save_registry, upsert_instance}; 35 36 pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management"; 37 38 pub fn parse_contract_str( 39 raw: &str, 40 ) -> Result<RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError> { 41 let contract = toml::from_str::<RadrootsRuntimeManagementContract>(raw) 42 .map_err(|err| RadrootsRuntimeManagerError::Parse(err.to_string()))?; 43 if contract.schema != RUNTIME_MANAGEMENT_SCHEMA { 44 return Err(RadrootsRuntimeManagerError::UnexpectedSchema { 45 expected: RUNTIME_MANAGEMENT_SCHEMA, 46 found: contract.schema.clone(), 47 }); 48 } 49 Ok(contract) 50 } 51 52 #[cfg(test)] 53 mod tests { 54 use std::path::PathBuf; 55 56 use radroots_runtime_paths::{ 57 RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, 58 RadrootsPlatform, 59 }; 60 use tempfile::tempdir; 61 62 use crate::{ 63 ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, 64 bootstrap_runtime, instance, load_registry, parse_contract_str, resolve_instance_paths, 65 resolve_shared_paths, save_registry, upsert_instance, 66 }; 67 68 fn assert_error_contains(err: &crate::RadrootsRuntimeManagerError, parts: &[&str]) { 69 let rendered = err.to_string(); 70 for part in parts { 71 assert!( 72 rendered.contains(part), 73 "expected `{rendered}` to contain `{part}`" 74 ); 75 } 76 } 77 78 const CONTRACT: &str = r#" 79 schema = "radroots-runtime-management" 80 schema_version = 1 81 owner_doc = "docs/execution/rcl/radroots-modular-runtime-management-bootstrap-rcl.md" 82 runtime_registry = "registry.toml" 83 distribution_contract = "distribution.toml" 84 capabilities_contract = "capabilities.toml" 85 86 [defaults] 87 instance_cardinality = "single_default_instance" 88 managed_runtime_lookup = "shared_instance_registry" 89 explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true 90 global_path_mutation_forbidden = true 91 92 [management_clients] 93 active = ["cli"] 94 defined = ["community-app-desktop"] 95 96 [managed_runtime_targets] 97 active = ["radrootsd"] 98 defined = ["myc", "rhi"] 99 bootstrap_only = ["hyf"] 100 101 [lifecycle] 102 actions = ["install", "uninstall", "start", "stop", "restart", "status", "logs", "config_show", "config_set"] 103 destructive_actions = ["uninstall"] 104 health_states = ["not_installed", "stopped", "starting", "running", "degraded", "failed"] 105 106 [mode.interactive_user_managed] 107 contract_state = "active" 108 platforms = ["linux", "macos", "windows"] 109 supported_profiles = ["interactive_user", "repo_local"] 110 service_manager_integration = false 111 uses_absolute_binary_paths = true 112 requires_explicit_pid_tracking = true 113 requires_explicit_log_tracking = true 114 default_instance_cardinality = "single_default_instance" 115 116 [mode.service_host_managed] 117 contract_state = "defined" 118 platforms = ["linux", "macos", "windows"] 119 supported_profiles = ["service_host"] 120 service_manager_integration = true 121 uses_absolute_binary_paths = true 122 default_instance_cardinality = "single_default_instance" 123 124 [paths.interactive_user_managed] 125 shared_namespace = "shared/runtime-manager" 126 instance_registry_root_class = "config" 127 instance_registry_rel = "shared/runtime-manager/instances.toml" 128 artifact_cache_root_class = "cache" 129 artifact_cache_rel = "shared/runtime-manager/artifacts" 130 install_root_class = "data" 131 install_root_rel = "shared/runtime-manager/installs" 132 state_root_class = "data" 133 state_root_rel = "shared/runtime-manager/state" 134 logs_root_class = "logs" 135 logs_root_rel = "shared/runtime-manager" 136 run_root_class = "run" 137 run_root_rel = "shared/runtime-manager" 138 secrets_root_class = "secrets" 139 secrets_namespace_rel = "shared/runtime-manager" 140 141 [instance_metadata] 142 required_fields = [ 143 "runtime_id", 144 "instance_id", 145 "management_mode", 146 "install_state", 147 "binary_path", 148 "config_path", 149 "logs_path", 150 "run_path", 151 "installed_version", 152 ] 153 optional_fields = [ 154 "health_endpoint", 155 "secret_material_ref", 156 "last_started_at", 157 "last_stopped_at", 158 "notes", 159 ] 160 161 [bootstrap.radrootsd] 162 runtime_id = "radrootsd" 163 management_mode = "interactive_user_managed" 164 default_instance_id = "local" 165 install_strategy = "archive_unpack" 166 config_format = "toml" 167 requires_bootstrap_secret = true 168 requires_config_bootstrap = true 169 requires_signer_provider = false 170 health_surface = "jsonrpc_status" 171 preferred_cli_binding = true 172 "#; 173 174 #[test] 175 fn parse_contract_accepts_expected_schema() { 176 let contract = parse_contract_str(CONTRACT).expect("parse contract"); 177 assert_eq!(contract.schema, crate::RUNTIME_MANAGEMENT_SCHEMA); 178 assert!(contract.mode.contains_key("interactive_user_managed")); 179 } 180 181 #[test] 182 fn parse_contract_reports_invalid_toml() { 183 let err = parse_contract_str("schema = [").expect_err("invalid toml should fail"); 184 assert_error_contains(&err, &["parse runtime management contract"]); 185 } 186 187 #[test] 188 fn parse_contract_rejects_unexpected_schema() { 189 let err = parse_contract_str(&CONTRACT.replace( 190 "schema = \"radroots-runtime-management\"", 191 "schema = \"wrong-schema\"", 192 )) 193 .expect_err("unexpected schema should fail"); 194 assert_error_contains(&err, &["wrong-schema", crate::RUNTIME_MANAGEMENT_SCHEMA]); 195 } 196 197 #[test] 198 fn resolve_shared_paths_uses_interactive_user_roots() { 199 let contract = parse_contract_str(CONTRACT).expect("parse contract"); 200 let resolver = RadrootsPathResolver::new( 201 RadrootsPlatform::Linux, 202 RadrootsHostEnvironment { 203 home_dir: Some(PathBuf::from("/home/treesap")), 204 ..RadrootsHostEnvironment::default() 205 }, 206 ); 207 208 let paths = resolve_shared_paths( 209 &contract, 210 &resolver, 211 RadrootsPathProfile::InteractiveUser, 212 &RadrootsPathOverrides::default(), 213 "interactive_user_managed", 214 ) 215 .expect("resolve shared manager paths"); 216 217 assert_eq!( 218 paths.instance_registry_path, 219 PathBuf::from("/home/treesap/.radroots/config/shared/runtime-manager/instances.toml") 220 ); 221 assert_eq!( 222 paths.install_root, 223 PathBuf::from("/home/treesap/.radroots/data/shared/runtime-manager/installs") 224 ); 225 assert_eq!( 226 paths.artifact_cache_dir, 227 PathBuf::from("/home/treesap/.radroots/cache/shared/runtime-manager/artifacts") 228 ); 229 assert_eq!( 230 paths.state_root, 231 PathBuf::from("/home/treesap/.radroots/data/shared/runtime-manager/state") 232 ); 233 assert_eq!( 234 paths.logs_root, 235 PathBuf::from("/home/treesap/.radroots/logs/shared/runtime-manager") 236 ); 237 assert_eq!( 238 paths.run_root, 239 PathBuf::from("/home/treesap/.radroots/run/shared/runtime-manager") 240 ); 241 assert_eq!( 242 paths.secrets_root, 243 PathBuf::from("/home/treesap/.radroots/secrets/shared/runtime-manager") 244 ); 245 } 246 247 #[test] 248 fn resolve_repo_local_paths_uses_explicit_base_root() { 249 let contract = parse_contract_str(CONTRACT).expect("parse contract"); 250 let resolver = 251 RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); 252 253 let paths = resolve_shared_paths( 254 &contract, 255 &resolver, 256 RadrootsPathProfile::RepoLocal, 257 &RadrootsPathOverrides::repo_local("/repo/.local/radroots"), 258 "interactive_user_managed", 259 ) 260 .expect("resolve repo local manager paths"); 261 262 assert_eq!( 263 paths.state_root, 264 PathBuf::from("/repo/.local/radroots/data/shared/runtime-manager/state") 265 ); 266 } 267 268 #[test] 269 fn resolve_instance_paths_builds_per_runtime_layout() { 270 let contract = parse_contract_str(CONTRACT).expect("parse contract"); 271 let resolver = RadrootsPathResolver::new( 272 RadrootsPlatform::Macos, 273 RadrootsHostEnvironment { 274 home_dir: Some(PathBuf::from("/Users/treesap")), 275 ..RadrootsHostEnvironment::default() 276 }, 277 ); 278 let shared = resolve_shared_paths( 279 &contract, 280 &resolver, 281 RadrootsPathProfile::InteractiveUser, 282 &RadrootsPathOverrides::default(), 283 "interactive_user_managed", 284 ) 285 .expect("resolve shared manager paths"); 286 287 let instance_paths = resolve_instance_paths(&shared, "radrootsd", "local"); 288 assert_eq!( 289 instance_paths.install_dir, 290 PathBuf::from( 291 "/Users/treesap/.radroots/data/shared/runtime-manager/installs/radrootsd/local" 292 ) 293 ); 294 assert_eq!( 295 instance_paths.pid_file_path, 296 PathBuf::from( 297 "/Users/treesap/.radroots/run/shared/runtime-manager/radrootsd/local/runtime.pid" 298 ) 299 ); 300 assert_eq!( 301 instance_paths.metadata_path, 302 PathBuf::from( 303 "/Users/treesap/.radroots/data/shared/runtime-manager/state/radrootsd/local/instance.toml" 304 ) 305 ); 306 } 307 308 #[test] 309 fn registry_round_trip_persists_and_reloads_instances() { 310 let dir = tempdir().expect("tempdir"); 311 let registry_path = dir.path().join("instances.toml"); 312 let mut registry = crate::ManagedRuntimeInstanceRegistry::default(); 313 upsert_instance( 314 &mut registry, 315 ManagedRuntimeInstanceRecord { 316 runtime_id: "radrootsd".to_string(), 317 instance_id: "local".to_string(), 318 management_mode: "interactive_user_managed".to_string(), 319 install_state: ManagedRuntimeInstallState::Configured, 320 binary_path: PathBuf::from("/tmp/radrootsd"), 321 config_path: PathBuf::from("/tmp/config.toml"), 322 logs_path: PathBuf::from("/tmp/logs"), 323 run_path: PathBuf::from("/tmp/run"), 324 installed_version: "0.1.0-alpha.2".to_string(), 325 health_endpoint: Some("jsonrpc_status".to_string()), 326 secret_material_ref: Some( 327 "shared/runtime-manager/radrootsd/local/token".to_string(), 328 ), 329 last_started_at: Some("2026-04-08T00:00:00Z".to_string()), 330 last_stopped_at: None, 331 notes: Some("test".to_string()), 332 }, 333 ); 334 335 save_registry(®istry_path, ®istry).expect("save registry"); 336 let reloaded = load_registry(®istry_path).expect("load registry"); 337 let record = instance(&reloaded, "radrootsd", "local").expect("instance record"); 338 assert_eq!(record.install_state, ManagedRuntimeInstallState::Configured); 339 assert_eq!(record.health_endpoint.as_deref(), Some("jsonrpc_status")); 340 } 341 342 #[test] 343 fn bootstrap_lookup_returns_radrootsd_contract() { 344 let contract = parse_contract_str(CONTRACT).expect("parse contract"); 345 let bootstrap = bootstrap_runtime(&contract, "radrootsd").expect("bootstrap contract"); 346 assert_eq!(bootstrap.default_instance_id, "local"); 347 assert_eq!(bootstrap.health_surface, "jsonrpc_status"); 348 assert!(bootstrap.preferred_cli_binding); 349 } 350 351 #[test] 352 fn install_and_health_state_surface_is_typed() { 353 assert_eq!( 354 ManagedRuntimeInstallState::Installed, 355 ManagedRuntimeInstallState::Installed 356 ); 357 assert_eq!( 358 ManagedRuntimeHealthState::Degraded, 359 ManagedRuntimeHealthState::Degraded 360 ); 361 } 362 }