lib

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

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(&registry_path, &registry).expect("save registry");
    336         let reloaded = load_registry(&registry_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 }