lib

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

commit 13a8c2c08441ec4ae069ce083c2b290300018061
parent 5b0f2d9bcabada0beb8768356c3ac354b9c20aa5
Author: triesap <tyson@radroots.org>
Date:   Sun, 16 Aug 2026 03:03:25 +0000

runtime-manager: quarantine legacy lifecycle authority

Diffstat:
MCargo.lock | 3---
Mcrates/runtime_manager/Cargo.toml | 7+------
Mcrates/runtime_manager/README | 31++++++++++---------------------
Mcrates/runtime_manager/src/error.rs | 60+-----------------------------------------------------------
Mcrates/runtime_manager/src/lib.rs | 112+++++++++++++++++++++++++++++++++++--------------------------------------------
Dcrates/runtime_manager/src/lifecycle.rs | 1387-------------------------------------------------------------------------------
Mcrates/runtime_manager/src/managed.rs | 1279++++++++++++-------------------------------------------------------------------
Mcrates/runtime_manager/src/model.rs | 139+++----------------------------------------------------------------------------
Dcrates/runtime_manager/src/paths.rs | 373-------------------------------------------------------------------------------
Dcrates/runtime_manager/src/registry.rs | 392-------------------------------------------------------------------------------
Mcrates/runtime_manager/tests/service_target_boundary.rs | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
11 files changed, 318 insertions(+), 3531 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3403,12 +3403,9 @@ dependencies = [ name = "radroots_runtime_manager" version = "0.1.0-alpha" dependencies = [ - "flate2", "radroots_runtime_distribution", "radroots_runtime_paths", "serde", - "tar", - "tempfile", "thiserror 1.0.69", "toml 0.8.23", ] diff --git a/crates/runtime_manager/Cargo.toml b/crates/runtime_manager/Cargo.toml @@ -6,20 +6,15 @@ edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true license.workspace = true -description = "Runtime lifecycle layer for Radroots" +description = "Metadata-only hardened service target resolver for Radroots" repository.workspace = true homepage.workspace = true documentation = "https://docs.rs/radroots_runtime_manager" readme = "README" [dependencies] -flate2 = { workspace = true } radroots_runtime_distribution = { workspace = true } radroots_runtime_paths = { workspace = true } serde = { workspace = true, features = ["derive"] } -tar = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } - -[dev-dependencies] -tempfile = { workspace = true } diff --git a/crates/runtime_manager/README b/crates/runtime_manager/README @@ -1,30 +1,19 @@ # radroots_runtime_manager -This is the README for `radroots_runtime_manager`, which provides managed -runtime lifecycle and inspection helpers for the `radroots` core libraries. +`radroots_runtime_manager` validates the frozen runtime-management contract and +resolves explicitly selected Myc and RHI service-instance identities to sealed, +metadata-only targets. -## Overview - - * schema-checked management contract parsing and shared management constants; - * manager-owned shared and per-instance tracking paths kept separate from - canonical service-instance paths supplied by a sealed `RuntimeContext`; - * typed, deterministic registry load, save, and lookup helpers that - persist service/instance identities without duplicating service paths or - secrets; - * lifecycle helpers for contained archive install, process start and stop, - and context-bound non-secret configuration writes; the manager has no - credential read/write authority; - * sealed Myc/RHI target metadata and context resolution without service - registration, lifecycle admission, or PID/config/log probing; and - * cleanup behavior limited to manager-owned install and tracking artifacts, -preserving canonical service state and secrets. +The crate performs no filesystem, registry, process, archive, artifact, +configuration, log, PID, install, removal, or lifecycle work. It exposes no +runtime paths or raw persistence helpers. Final service lifecycle and artifact +behavior remains unavailable until the separately governed Steps 219 and 220. Complete management TOML documents are rejected before parsing when they exceed exactly 1,048,576 UTF-8 bytes. Bounded documents still require the exact -schema, version, closed field set, and complete hardened-service inventory. - -Myc and RHI remain metadata-only management targets until their public CLI, -Unix-admin, status, and artifact integrations are separately implemented. +schema, version, closed field set, empty lifecycle inventory, and complete +Myc/RHI metadata inventory. Directly constructed contracts are revalidated +before a management context can be created. ## Copyright diff --git a/crates/runtime_manager/src/error.rs b/crates/runtime_manager/src/error.rs @@ -1,10 +1,6 @@ use thiserror::Error; -/// Stable, path-free runtime-management failures. -/// -/// Filesystem paths, file contents, and dependency-owned causes are retained -/// only inside the operation that handles them. Ordinary `Display`, `Debug`, -/// and error-chain traversal therefore cannot disclose them. +/// Stable, value-free runtime-management metadata failures. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum RadrootsRuntimeManagerError { #[error("runtime management contract exceeds its size limit")] @@ -19,60 +15,6 @@ pub enum RadrootsRuntimeManagerError { InvalidContract, #[error("management mode does not support the selected profile")] UnsupportedProfile, - #[error("runtime has no bootstrap entry in runtime management contract")] - UnknownBootstrapRuntime, #[error("runtime is not a hardened service target")] UnsupportedServiceTarget, - #[error("hardened service target is metadata-only until service integration is complete")] - MetadataOnlyServiceTarget, - #[error("runtime context does not share the manager path scope")] - RuntimeContextMismatch, - #[error("read runtime instance registry failed: {kind}")] - ReadRegistry { kind: std::io::ErrorKind }, - #[error("parse runtime instance registry failed")] - ParseRegistry, - #[error("runtime instance registry schema is unsupported")] - UnexpectedRegistrySchema, - #[error("runtime instance registry version is unsupported")] - UnexpectedRegistryVersion, - #[error("runtime instance registry contains a duplicate service instance")] - DuplicateRegistryInstance, - #[error("serialize runtime instance registry failed")] - SerializeRegistry, - #[error("create runtime instance registry parent failed: {kind}")] - CreateRegistryParent { kind: std::io::ErrorKind }, - #[error("write runtime instance registry failed: {kind}")] - WriteRegistry { kind: std::io::ErrorKind }, - #[error("create managed runtime directory failed: {kind}")] - CreateDirectory { kind: std::io::ErrorKind }, - #[error("copy managed runtime binary failed: {kind}")] - CopyBinary { kind: std::io::ErrorKind }, - #[error("write managed runtime config failed: {kind}")] - WriteManagedConfig { kind: std::io::ErrorKind }, - #[error("read managed runtime file failed: {kind}")] - ReadManagedFile { kind: std::io::ErrorKind }, - #[error("open managed runtime log failed: {kind}")] - OpenLogFile { kind: std::io::ErrorKind }, - #[error("spawn managed runtime process failed: {kind}")] - SpawnProcess { kind: std::io::ErrorKind }, - #[error("write managed runtime pid failed: {kind}")] - WritePidFile { kind: std::io::ErrorKind }, - #[error("read managed runtime pid failed: {kind}")] - ReadPidFile { kind: std::io::ErrorKind }, - #[error("managed runtime pid is malformed")] - ParsePidFile, - #[error("remove manager-owned runtime path failed: {kind}")] - RemovePath { kind: std::io::ErrorKind }, - #[error("set managed runtime file permissions failed: {kind}")] - SetPermissions { kind: std::io::ErrorKind }, - #[error("signal managed runtime process failed: {kind}")] - ExecuteProcessSignal { kind: std::io::ErrorKind }, - #[error("managed runtime process did not stop")] - StopProcess, - #[error("managed runtime archive format is unsupported")] - UnsupportedArchiveFormat, - #[error("unpack managed runtime archive failed: {kind}")] - UnpackArchive { kind: std::io::ErrorKind }, - #[error("managed runtime artifact name is invalid")] - InvalidArtifactName, } diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs @@ -1,34 +1,16 @@ #![forbid(unsafe_code)] -pub mod error; -pub mod lifecycle; -pub mod managed; -pub mod model; -pub mod paths; -pub mod registry; +mod error; +mod managed; +mod model; pub use error::RadrootsRuntimeManagerError; -pub use lifecycle::{ - ManagedRuntimeArtifactName, ensure_instance_layout, extract_binary_archive, install_binary, - process_running, remove_instance_artifacts, start_process, stop_process, write_instance_config, -}; -pub use managed::{ - 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, - resolve_runtime_target, runtime_group, -}; +pub use managed::{ManagedRuntimeContext, ManagedRuntimeTarget, resolve_runtime_target}; pub use model::{ - BootstrapRuntimeContract, LifecycleContract, ManagedRuntimeHealthState, - ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, - ManagementDefaults, ManagementModeContract, ManagementPathContract, - RUNTIME_INSTANCE_REGISTRY_SCHEMA, RUNTIME_INSTANCE_REGISTRY_VERSION, - RadrootsRuntimeManagementContract, RuntimeGroups, + BootstrapRuntimeContract, InstanceMetadataContract, LifecycleContract, ManagementDefaults, + ManagementModeContract, ManagementPathContract, RadrootsRuntimeManagementContract, + RuntimeGroups, }; -pub use paths::{ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstrap_runtime}; -pub use registry::{instance, load_registry, save_registry}; pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management"; pub const RUNTIME_MANAGEMENT_SCHEMA_VERSION: u32 = 1; @@ -79,15 +61,23 @@ mod tests { const CONTRACT: &str = HARDENED_MANAGEMENT_CONTRACT; #[test] - fn contract_parser_accepts_only_the_expected_schema() { + fn contract_parser_accepts_only_the_exact_static_inventory() { let contract = parse_contract_str(CONTRACT).expect("contract"); assert_eq!(contract.schema, RUNTIME_MANAGEMENT_SCHEMA); - - let wrong = CONTRACT.replace( - "schema = \"radroots-runtime-management\"", - "schema = \"wrong\"", - ); - assert!(parse_contract_str(&wrong).is_err()); + assert_eq!(contract.service_targets.len(), 2); + assert!(contract.bootstrap.is_empty()); + + for raw in [ + CONTRACT.replace("schema_version = 1", "schema_version = 2"), + CONTRACT.replace( + "defined = [\"myc\", \"rhi\"]", + "active = [\"myc\"]\ndefined = [\"rhi\"]", + ), + CONTRACT.replace("actions = []", "actions = [\"start\"]"), + format!("{CONTRACT}\nunknown = true\n"), + ] { + assert!(parse_contract_str(&raw).is_err()); + } assert!(parse_contract_str("schema = [").is_err()); } @@ -116,7 +106,7 @@ mod tests { } #[test] - fn contract_errors_redact_raw_schema_values_and_parser_causes() { + fn contract_errors_are_value_free_and_source_free() { for (raw, secret) in [ ( CONTRACT.replace( @@ -130,43 +120,39 @@ mod tests { "secret-value", ), ] { - let err = parse_contract_str(&raw).expect_err("invalid contract"); - let rendered = format!("{err} {err:?}"); + let error = parse_contract_str(&raw).expect_err("invalid contract"); + let rendered = format!("{error} {error:?}"); assert!(!rendered.contains(secret)); - assert!(err.source().is_none()); + assert!(error.source().is_none()); } } #[test] - fn manager_source_no_longer_consumes_legacy_path_selection_or_raw_identity_joins() { - let sources = [ - include_str!("error.rs"), - include_str!("lifecycle.rs"), - include_str!("managed.rs"), - include_str!("model.rs"), - include_str!("paths.rs"), - include_str!("registry.rs"), - ]; + fn root_surface_exposes_no_legacy_runtime_authority() { + let source = include_str!("lib.rs") + .split("\n#[cfg(test)]") + .next() + .expect("production source"); for forbidden in [ - "RadrootsRuntimePathSelection", - "load_management_context_with_selection", - "PathBuf::from(runtime_id).join(instance_id)", - "record.config_path", - "record.logs_path", - "record.run_path", - "workers/rhi", - "pub fn read_secret_file", - "pub fn write_secret_file", - "pub fn write_managed_file", - "pub fn registry_mut", - "pub fn upsert_instance", - "pub fn resolve_shared_paths", - "pub fn resolve_instance_paths", + "mod lifecycle", + "mod paths", + "mod registry", + "ManagedRuntimeArtifactName", + "ManagedRuntimeInstancePaths", + "ManagedRuntimeSharedPaths", + "ManagedRuntimeInstanceRegistry", + "ManagedRuntimeInstanceRecord", + "ManagedRuntimeLifecycleAction", + "load_registry", + "save_registry", + "start_process", + "stop_process", + "install_binary", + "extract_binary_archive", + "remove_instance_artifacts", + "write_instance_config", ] { - assert!( - sources.iter().all(|source| !source.contains(forbidden)), - "runtime manager retained forbidden source `{forbidden}`" - ); + assert!(!source.contains(forbidden), "root retained `{forbidden}`"); } } } diff --git a/crates/runtime_manager/src/lifecycle.rs b/crates/runtime_manager/src/lifecycle.rs @@ -1,1387 +0,0 @@ -use std::fs::{self, File, OpenOptions}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::thread; -use std::time::Duration; - -#[cfg(unix)] -use std::process::{ExitStatus, Output}; - -use flate2::read::GzDecoder; - -use crate::error::RadrootsRuntimeManagerError; -use crate::paths::ManagedRuntimeInstancePaths; - -type SpawnProcess = fn(&Path, &[String], &[(String, String)], File, File) -> std::io::Result<u32>; - -/// A validated single-component manager-owned executable artifact name. -#[derive(Clone, PartialEq, Eq)] -pub struct ManagedRuntimeArtifactName(String); - -impl ManagedRuntimeArtifactName { - pub fn new(value: &str) -> Result<Self, RadrootsRuntimeManagerError> { - if value.is_empty() - || value.len() > 128 - || !value.as_bytes()[0].is_ascii_alphanumeric() - || !value - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) - || Path::new(value).components().count() != 1 - { - return Err(RadrootsRuntimeManagerError::InvalidArtifactName); - } - Ok(Self(value.to_owned())) - } - - #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl core::fmt::Debug for ManagedRuntimeArtifactName { - fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter.write_str("ManagedRuntimeArtifactName([redacted])") - } -} - -pub fn ensure_instance_layout( - paths: &ManagedRuntimeInstancePaths, -) -> Result<(), RadrootsRuntimeManagerError> { - for path in [paths.install_dir(), paths.logs_dir(), paths.run_dir()] { - fs::create_dir_all(path).map_err(|source| { - RadrootsRuntimeManagerError::CreateDirectory { - kind: source.kind(), - } - })?; - } - Ok(()) -} - -pub fn install_binary( - source_binary_path: impl AsRef<Path>, - paths: &ManagedRuntimeInstancePaths, - binary_name: &ManagedRuntimeArtifactName, -) -> Result<PathBuf, RadrootsRuntimeManagerError> { - install_binary_path(source_binary_path.as_ref(), paths, binary_name) -} - -fn install_binary_path( - source_binary_path: &Path, - paths: &ManagedRuntimeInstancePaths, - binary_name: &ManagedRuntimeArtifactName, -) -> Result<PathBuf, RadrootsRuntimeManagerError> { - ensure_instance_layout(paths)?; - let installed_binary_path = paths.install_dir().join(binary_name.as_str()); - fs::copy(source_binary_path, &installed_binary_path).map_err(|source| { - RadrootsRuntimeManagerError::CopyBinary { - kind: source.kind(), - } - })?; - set_executable_mode(&installed_binary_path)?; - Ok(installed_binary_path) -} - -pub fn extract_binary_archive( - archive_path: impl AsRef<Path>, - archive_format: &str, - paths: &ManagedRuntimeInstancePaths, - binary_name: &ManagedRuntimeArtifactName, -) -> Result<PathBuf, RadrootsRuntimeManagerError> { - extract_binary_archive_path(archive_path.as_ref(), archive_format, paths, binary_name) -} - -fn extract_binary_archive_path( - archive_path: &Path, - archive_format: &str, - paths: &ManagedRuntimeInstancePaths, - binary_name: &ManagedRuntimeArtifactName, -) -> Result<PathBuf, RadrootsRuntimeManagerError> { - remove_path_if_exists(paths.install_dir())?; - ensure_instance_layout(paths)?; - - match archive_format { - "tar.gz" => unpack_tar_gz_archive(archive_path, paths.install_dir())?, - _ => return Err(RadrootsRuntimeManagerError::UnsupportedArchiveFormat), - } - - let installed_binary_path = paths.install_dir().join(binary_name.as_str()); - let resolved_binary_path = if installed_binary_path.is_file() { - installed_binary_path - } else { - find_binary_with_name(paths.install_dir(), binary_name.as_str()).ok_or( - RadrootsRuntimeManagerError::ReadManagedFile { - kind: std::io::ErrorKind::NotFound, - }, - )? - }; - set_executable_mode(&resolved_binary_path)?; - Ok(resolved_binary_path) -} - -pub fn write_instance_config( - paths: &ManagedRuntimeInstancePaths, - contents: &str, -) -> Result<(), RadrootsRuntimeManagerError> { - let path = paths.config_path(); - ensure_parent_dir(&path)?; - fs::write(path, contents).map_err(|source| RadrootsRuntimeManagerError::WriteManagedConfig { - kind: source.kind(), - }) -} - -pub fn start_process( - paths: &ManagedRuntimeInstancePaths, - binary_name: &ManagedRuntimeArtifactName, - args: &[String], - envs: &[(String, String)], -) -> Result<u32, RadrootsRuntimeManagerError> { - start_process_path( - &paths.install_dir().join(binary_name.as_str()), - args, - envs, - paths, - ) -} - -fn start_process_path( - binary_path: &Path, - args: &[String], - envs: &[(String, String)], - paths: &ManagedRuntimeInstancePaths, -) -> Result<u32, RadrootsRuntimeManagerError> { - start_process_with(binary_path, args, envs, paths, spawn_process) -} - -fn start_process_with( - binary_path: &Path, - args: &[String], - envs: &[(String, String)], - paths: &ManagedRuntimeInstancePaths, - spawn: SpawnProcess, -) -> Result<u32, RadrootsRuntimeManagerError> { - ensure_instance_layout(paths)?; - let stdout = open_log_file(paths.stdout_log_path())?; - let stderr = open_log_file(paths.stderr_log_path())?; - let pid = spawn(binary_path, args, envs, stdout, stderr).map_err(|source| { - RadrootsRuntimeManagerError::SpawnProcess { - kind: source.kind(), - } - })?; - fs::write(paths.pid_file_path(), pid.to_string()).map_err(|source| { - RadrootsRuntimeManagerError::WritePidFile { - kind: source.kind(), - } - })?; - Ok(pid) -} - -fn spawn_process( - binary_path: &Path, - args: &[String], - envs: &[(String, String)], - stdout: File, - stderr: File, -) -> std::io::Result<u32> { - Command::new(binary_path) - .args(args) - .envs(envs.iter().map(|(key, value)| (key, value))) - .stdin(Stdio::null()) - .stdout(Stdio::from(stdout)) - .stderr(Stdio::from(stderr)) - .spawn() - .map(|child| child.id()) -} - -pub fn process_running( - paths: &ManagedRuntimeInstancePaths, -) -> Result<bool, RadrootsRuntimeManagerError> { - let Some(pid) = read_pid(paths)? else { - return Ok(false); - }; - Ok(process_running_for_pid(pid)) -} - -pub fn stop_process( - paths: &ManagedRuntimeInstancePaths, -) -> Result<bool, RadrootsRuntimeManagerError> { - let Some(pid) = read_pid(paths)? else { - return Ok(false); - }; - if !process_running_for_pid(pid) { - remove_pid_file(paths)?; - return Ok(false); - } - - let mut is_running = process_running_for_pid; - let mut terminate = terminate_process; - let mut force_kill = force_kill_process; - let mut sleep = thread::sleep; - stop_process_for_pid( - paths, - pid, - &mut is_running, - &mut terminate, - &mut force_kill, - &mut sleep, - ) -} - -pub fn remove_instance_artifacts( - paths: &ManagedRuntimeInstancePaths, -) -> Result<(), RadrootsRuntimeManagerError> { - for path in [paths.install_dir(), paths.logs_dir(), paths.run_dir()] { - remove_path_if_exists(path)?; - } - Ok(()) -} - -fn stop_process_for_pid( - paths: &ManagedRuntimeInstancePaths, - pid: u32, - is_running: &mut dyn FnMut(u32) -> bool, - terminate: &mut dyn FnMut(u32) -> Result<(), RadrootsRuntimeManagerError>, - force_kill: &mut dyn FnMut(u32) -> Result<(), RadrootsRuntimeManagerError>, - sleep: &mut dyn FnMut(Duration), -) -> Result<bool, RadrootsRuntimeManagerError> { - terminate(pid)?; - for _ in 0..20 { - if !is_running(pid) { - remove_pid_file(paths)?; - return Ok(true); - } - sleep(Duration::from_millis(100)); - } - - force_kill(pid)?; - for _ in 0..20 { - if !is_running(pid) { - remove_pid_file(paths)?; - return Ok(true); - } - sleep(Duration::from_millis(100)); - } - - Err(RadrootsRuntimeManagerError::StopProcess) -} - -fn unpack_tar_gz_archive( - archive_path: &Path, - destination_dir: &Path, -) -> Result<(), RadrootsRuntimeManagerError> { - let archive_file = File::open(archive_path).map_err(|source| { - RadrootsRuntimeManagerError::ReadManagedFile { - kind: source.kind(), - } - })?; - let decoder = GzDecoder::new(archive_file); - let mut archive = tar::Archive::new(decoder); - archive - .unpack(destination_dir) - .map_err(|source| RadrootsRuntimeManagerError::UnpackArchive { - kind: source.kind(), - }) -} - -fn find_binary_with_name(root: &Path, binary_name: &str) -> Option<PathBuf> { - let entries = fs::read_dir(root).ok()?; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - if let Some(found) = find_binary_with_name(&path, binary_name) { - return Some(found); - } - continue; - } - if path.file_name().and_then(|name| name.to_str()) == Some(binary_name) { - return Some(path); - } - } - None -} - -fn open_log_file(path: &Path) -> Result<File, RadrootsRuntimeManagerError> { - ensure_parent_dir(path)?; - OpenOptions::new() - .create(true) - .append(true) - .open(path) - .map_err(|source| RadrootsRuntimeManagerError::OpenLogFile { - kind: source.kind(), - }) -} - -fn ensure_parent_dir(path: &Path) -> Result<(), RadrootsRuntimeManagerError> { - let Some(parent) = path.parent() else { - return Ok(()); - }; - fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateDirectory { - kind: source.kind(), - }) -} - -fn read_pid( - paths: &ManagedRuntimeInstancePaths, -) -> Result<Option<u32>, RadrootsRuntimeManagerError> { - let raw = match fs::read_to_string(paths.pid_file_path()) { - Ok(raw) => raw, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(source) => { - return Err(RadrootsRuntimeManagerError::ReadPidFile { - kind: source.kind(), - }); - } - }; - let trimmed = raw.trim(); - if trimmed.is_empty() { - return Ok(None); - } - trimmed - .parse::<u32>() - .map(Some) - .map_err(|_| RadrootsRuntimeManagerError::ParsePidFile) -} - -fn remove_pid_file(paths: &ManagedRuntimeInstancePaths) -> Result<(), RadrootsRuntimeManagerError> { - match fs::remove_file(paths.pid_file_path()) { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(source) => Err(RadrootsRuntimeManagerError::RemovePath { - kind: source.kind(), - }), - } -} - -fn remove_path_if_exists(path: &Path) -> Result<(), RadrootsRuntimeManagerError> { - let state = match fs::metadata(path) { - Ok(metadata) if metadata.is_dir() => Ok(Some(ExistingPathKind::Directory)), - Ok(_) => Ok(Some(ExistingPathKind::File)), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(source) => Err(source), - }; - remove_path_from_state(path, state, remove_dir_all_path, remove_file_path) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ExistingPathKind { - Directory, - File, -} - -fn remove_path_from_state( - path: &Path, - state: Result<Option<ExistingPathKind>, std::io::Error>, - remove_dir_all: fn(&Path) -> std::io::Result<()>, - remove_file: fn(&Path) -> std::io::Result<()>, -) -> Result<(), RadrootsRuntimeManagerError> { - match state { - Ok(Some(ExistingPathKind::Directory)) => { - remove_dir_all(path).map_err(|source| RadrootsRuntimeManagerError::RemovePath { - kind: source.kind(), - }) - } - Ok(Some(ExistingPathKind::File)) => { - remove_file(path).map_err(|source| RadrootsRuntimeManagerError::RemovePath { - kind: source.kind(), - }) - } - Ok(None) => Ok(()), - Err(source) => Err(RadrootsRuntimeManagerError::ReadManagedFile { - kind: source.kind(), - }), - } -} - -fn remove_dir_all_path(path: &Path) -> std::io::Result<()> { - fs::remove_dir_all(path) -} - -fn remove_file_path(path: &Path) -> std::io::Result<()> { - fs::remove_file(path) -} - -#[cfg(unix)] -fn set_executable_mode(path: &Path) -> Result<(), RadrootsRuntimeManagerError> { - apply_mode(path, 0o755, set_permissions_path) -} - -#[cfg(not(unix))] -fn set_executable_mode(_path: &Path) -> Result<(), RadrootsRuntimeManagerError> { - Ok(()) -} - -#[cfg(unix)] -fn apply_mode( - path: &Path, - mode: u32, - set_permissions: fn(&Path, fs::Permissions) -> std::io::Result<()>, -) -> Result<(), RadrootsRuntimeManagerError> { - use std::os::unix::fs::PermissionsExt; - - let metadata = - fs::metadata(path).map_err(|source| RadrootsRuntimeManagerError::ReadManagedFile { - kind: source.kind(), - })?; - let mut permissions = metadata.permissions(); - permissions.set_mode(mode); - set_permissions(path, permissions).map_err(|source| { - RadrootsRuntimeManagerError::SetPermissions { - kind: source.kind(), - } - }) -} - -#[cfg(unix)] -fn set_permissions_path(path: &Path, permissions: fs::Permissions) -> std::io::Result<()> { - fs::set_permissions(path, permissions) -} - -#[cfg(unix)] -fn process_running_for_pid(pid: u32) -> bool { - let pid_arg = pid.to_string(); - let running = Command::new("kill") - .args(["-0", pid_arg.as_str()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|status| status.success()) - .unwrap_or(false); - if !running { - return false; - } - - ps_output_for_pid(pid_arg.as_str()) - .map(process_running_state_from_ps_output) - .unwrap_or(true) -} - -#[cfg(unix)] -fn ps_output_for_pid(pid_arg: &str) -> std::io::Result<Output> { - Command::new("ps") - .args(["-o", "stat=", "-p", pid_arg]) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .output() -} - -#[cfg(unix)] -fn process_running_state_from_ps_output(output: Output) -> bool { - if !output.status.success() { - return true; - } - let state = String::from_utf8_lossy(output.stdout.as_slice()); - !state.trim_start().starts_with('Z') -} - -#[cfg(windows)] -fn process_running_for_pid(pid: u32) -> bool { - Command::new("tasklist") - .args(["/FI", format!("PID eq {pid}").as_str()]) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .output() - .map(|output| { - output.status.success() - && String::from_utf8_lossy(output.stdout.as_slice()) - .contains(pid.to_string().as_str()) - }) - .unwrap_or(false) -} - -#[cfg(not(any(unix, windows)))] -fn process_running_for_pid(_pid: u32) -> bool { - false -} - -#[cfg(unix)] -fn terminate_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - signal_process(pid, "-TERM") -} - -#[cfg(unix)] -fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - signal_process(pid, "-KILL") -} - -#[cfg(unix)] -fn signal_process(pid: u32, signal: &str) -> Result<(), RadrootsRuntimeManagerError> { - signal_process_with(pid, signal, execute_signal_command) -} - -#[cfg(unix)] -fn execute_signal_command(pid: u32, signal: &str) -> std::io::Result<ExitStatus> { - Command::new("kill") - .args([signal, pid.to_string().as_str()]) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .status() -} - -#[cfg(unix)] -fn signal_process_with( - pid: u32, - signal: &str, - runner: fn(u32, &str) -> std::io::Result<ExitStatus>, -) -> Result<(), RadrootsRuntimeManagerError> { - let status = runner(pid, signal).map_err(|source| { - RadrootsRuntimeManagerError::ExecuteProcessSignal { - kind: source.kind(), - } - })?; - if status.success() { - Ok(()) - } else { - Err(RadrootsRuntimeManagerError::StopProcess) - } -} - -#[cfg(windows)] -fn terminate_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - force_kill_process(pid) -} - -#[cfg(windows)] -fn force_kill_process(pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - let status = Command::new("taskkill") - .args(["/PID", pid.to_string().as_str(), "/T", "/F"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map_err(|source| RadrootsRuntimeManagerError::ExecuteProcessSignal { - kind: source.kind(), - })?; - if status.success() { - Ok(()) - } else { - Err(RadrootsRuntimeManagerError::StopProcess) - } -} - -#[cfg(not(any(unix, windows)))] -fn terminate_process(_pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - Err(RadrootsRuntimeManagerError::StopProcess) -} - -#[cfg(not(any(unix, windows)))] -fn force_kill_process(_pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - Err(RadrootsRuntimeManagerError::StopProcess) -} - -#[cfg(test)] -mod tests { - use std::fs; - #[cfg(unix)] - use std::fs::File; - use std::io; - use std::path::Path; - #[cfg(unix)] - use std::process::ExitStatus; - #[cfg(unix)] - use std::thread; - use std::time::Duration; - - use radroots_runtime_paths::{ - InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, - }; - use tempfile::tempdir; - - use super::{ - ExistingPathKind, ManagedRuntimeArtifactName, ensure_instance_layout, ensure_parent_dir, - extract_binary_archive, find_binary_with_name, install_binary, open_log_file, - process_running, read_pid, remove_instance_artifacts, remove_path_from_state, - remove_path_if_exists, start_process_with, stop_process, stop_process_for_pid, - write_instance_config, - }; - #[cfg(unix)] - use super::{ - apply_mode, force_kill_process, process_running_for_pid, - process_running_state_from_ps_output, set_executable_mode, signal_process, - signal_process_with, start_process, terminate_process, - }; - use crate::error::RadrootsRuntimeManagerError; - use crate::paths::{ManagedRuntimeInstancePaths, resolve_instance_paths, resolve_shared_paths}; - - fn sample_paths(root: &Path) -> ManagedRuntimeInstancePaths { - fn context(root: &Path, service: &str) -> RuntimeContext { - RuntimeContext::resolve( - &RadrootsPathResolver::new( - RadrootsPlatform::Linux, - RadrootsHostEnvironment::default(), - ), - RuntimeContextBootstrap::new( - RadrootsPathProfile::RepoLocal, - Some(root.to_path_buf()), - RuntimeContextSource::BootstrapCli, - RuntimeContextSource::BootstrapCli, - ) - .expect("bootstrap"), - ServiceId::new(service).expect("service"), - InstanceId::new("local").expect("instance"), - ) - .expect("context") - } - - let shared = resolve_shared_paths(&context(root, "runtime-manager")); - resolve_instance_paths(&shared, &context(root, "radrootsd")) - } - - fn artifact(value: &str) -> ManagedRuntimeArtifactName { - ManagedRuntimeArtifactName::new(value).expect("artifact name") - } - - fn assert_safe_error(err: &RadrootsRuntimeManagerError, expected: &str, forbidden: &[&str]) { - use std::error::Error as _; - - let rendered = format!("{err} {err:?}"); - assert!( - rendered.contains(expected), - "expected `{rendered}` to contain `{expected}`" - ); - for part in forbidden { - assert!( - !rendered.contains(part), - "expected `{rendered}` not to contain `{part}`" - ); - } - assert!(err.source().is_none()); - } - - #[cfg(unix)] - fn exit_status(code: i32) -> ExitStatus { - std::process::Command::new("sh") - .args(["-c", &format!("exit {code}")]) - .status() - .expect("exit status") - } - - #[cfg(unix)] - fn output_with_status(status: ExitStatus, stdout: &[u8]) -> std::process::Output { - std::process::Output { - status, - stdout: stdout.to_vec(), - stderr: Vec::new(), - } - } - - fn ok_remove_path(_path: &Path) -> io::Result<()> { - Ok(()) - } - - fn deny_remove_path(_path: &Path) -> io::Result<()> { - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "remove path denied", - )) - } - - fn ok_runtime_signal(_pid: u32) -> Result<(), RadrootsRuntimeManagerError> { - Ok(()) - } - - fn noop_runtime_sleep(_duration: Duration) {} - - fn runtime_is_stopped(_pid: u32) -> bool { - false - } - - fn runtime_is_running(_pid: u32) -> bool { - true - } - - #[test] - fn layout_creates_only_manager_owned_install_and_tracking_roots() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - assert!(paths.install_dir().is_dir()); - assert!(paths.logs_dir().is_dir()); - assert!(paths.run_dir().is_dir()); - assert!(!paths.config_dir().exists()); - assert!(!paths.state_dir().exists()); - assert!(!paths.secrets_dir().exists()); - } - - #[test] - fn install_binary_copies_source_into_install_dir() { - let dir = tempdir().expect("tempdir"); - let source = dir.path().join("radrootsd"); - fs::write(&source, "#!/bin/sh\nexit 0\n").expect("source"); - let paths = sample_paths(dir.path()); - let installed = install_binary(&source, &paths, &artifact("radrootsd")).expect("install"); - assert!(installed.is_file()); - assert!(installed.starts_with(paths.install_dir())); - } - - #[test] - fn artifact_names_reject_absolute_parent_and_multicomponent_escapes() { - for invalid in [ - "", - "/tmp/escape", - "../escape", - "nested/escape", - r"nested\escape", - ".", - "..", - " secret", - ] { - let err = ManagedRuntimeArtifactName::new(invalid).expect_err("reject artifact name"); - if invalid.is_empty() { - assert_safe_error(&err, "artifact name is invalid", &[]); - } else { - assert_safe_error(&err, "artifact name is invalid", &[invalid]); - } - } - - let maximum = format!("a{}", "b".repeat(127)); - assert!(ManagedRuntimeArtifactName::new(&maximum).is_ok()); - assert!(ManagedRuntimeArtifactName::new(&format!("{maximum}c")).is_err()); - } - - #[cfg(unix)] - #[test] - fn extract_binary_archive_unpacks_tar_gz() { - let dir = tempdir().expect("tempdir"); - let archive_root = dir.path().join("archive"); - fs::create_dir_all(archive_root.join("bin")).expect("archive dir"); - fs::write(archive_root.join("bin/radrootsd"), "#!/bin/sh\nexit 0\n").expect("binary"); - let archive_path = dir.path().join("radrootsd.tar.gz"); - let file = File::create(&archive_path).expect("archive file"); - let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); - let mut builder = tar::Builder::new(encoder); - builder - .append_path_with_name( - archive_root.join("bin/radrootsd"), - "radrootsd/bin/radrootsd", - ) - .expect("append path"); - builder.finish().expect("finish archive"); - let encoder = builder.into_inner().expect("into encoder"); - encoder.finish().expect("finish gzip"); - - let paths = sample_paths(dir.path()); - let installed = - extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd")) - .expect("extract"); - assert!(installed.is_file()); - } - - #[cfg(unix)] - #[test] - fn extract_binary_archive_uses_direct_binary_when_present_at_root() { - let dir = tempdir().expect("tempdir"); - let archive_root = dir.path().join("archive"); - fs::create_dir_all(&archive_root).expect("archive dir"); - fs::write(archive_root.join("radrootsd"), "#!/bin/sh\nexit 0\n").expect("binary"); - let archive_path = dir.path().join("radrootsd.tar.gz"); - let file = File::create(&archive_path).expect("archive file"); - let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); - let mut builder = tar::Builder::new(encoder); - builder - .append_path_with_name(archive_root.join("radrootsd"), "radrootsd") - .expect("append path"); - builder.finish().expect("finish archive"); - let encoder = builder.into_inner().expect("into encoder"); - encoder.finish().expect("finish gzip"); - - let paths = sample_paths(dir.path()); - let installed = - extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd")) - .expect("extract"); - assert_eq!(installed, paths.install_dir().join("radrootsd")); - } - - #[cfg(unix)] - #[test] - fn start_and_stop_process_manage_pid_file() { - let dir = tempdir().expect("tempdir"); - let binary = dir.path().join("sleepy.sh"); - fs::write(&binary, "#!/bin/sh\nexec sleep 30\n").expect("script"); - let paths = sample_paths(dir.path()); - install_binary(&binary, &paths, &artifact("sleepy.sh")).expect("install"); - let envs = vec![("RADROOTS_RUNTIME_MANAGER_TEST".to_owned(), "1".to_owned())]; - let pid = start_process(&paths, &artifact("sleepy.sh"), &Vec::new(), &envs).expect("start"); - assert!(pid > 0); - thread::sleep(Duration::from_millis(100)); - assert!(paths.pid_file_path().is_file()); - assert!(process_running(&paths).expect("running")); - assert!(stop_process(&paths).expect("stop")); - assert!(!paths.pid_file_path().exists()); - } - - #[test] - fn remove_instance_artifacts_removes_layout_roots() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::create_dir_all(paths.state_dir()).expect("canonical state sentinel"); - fs::create_dir_all(paths.secrets_dir()).expect("canonical secrets sentinel"); - fs::write(paths.state_dir().join("state.sqlite"), "state").expect("state sentinel"); - fs::write(paths.secrets_dir().join("identity.secret"), "secret").expect("secret sentinel"); - remove_instance_artifacts(&paths).expect("remove"); - assert!(!paths.install_dir().exists()); - assert!(!paths.logs_dir().exists()); - assert!(!paths.run_dir().exists()); - assert!(paths.state_dir().join("state.sqlite").is_file()); - assert!(paths.secrets_dir().join("identity.secret").is_file()); - } - - #[test] - fn ensure_instance_layout_reports_directory_errors() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - fs::create_dir_all(paths.install_dir().parent().expect("install parent")).expect("parent"); - fs::write(paths.install_dir(), "occupied").expect("file"); - - let err = ensure_instance_layout(&paths).expect_err("file path should fail"); - assert_safe_error( - &err, - "create managed runtime directory", - &[paths.install_dir().to_string_lossy().as_ref()], - ); - } - - #[test] - fn install_binary_reports_copy_errors() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - let err = install_binary(dir.path().join("missing"), &paths, &artifact("radrootsd")) - .expect_err("missing source should fail"); - assert_safe_error( - &err, - "copy managed runtime binary", - &[ - dir.path().join("missing").to_string_lossy().as_ref(), - paths - .install_dir() - .join("radrootsd") - .to_string_lossy() - .as_ref(), - ], - ); - } - - #[test] - fn extract_binary_archive_reports_unsupported_format() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - let archive_path = dir.path().join("radrootsd.zip"); - - let err = extract_binary_archive(&archive_path, "zip", &paths, &artifact("radrootsd")) - .expect_err("unsupported archive format should fail"); - assert_safe_error( - &err, - "archive format is unsupported", - &[archive_path.to_string_lossy().as_ref(), "zip"], - ); - } - - #[test] - fn extract_binary_archive_reports_missing_archive() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - let archive_path = dir.path().join("missing.tar.gz"); - - let err = extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd")) - .expect_err("missing archive should fail"); - assert_safe_error( - &err, - "read managed runtime file", - &[archive_path.to_string_lossy().as_ref()], - ); - } - - #[cfg(unix)] - #[test] - fn extract_binary_archive_reports_missing_binary_in_archive() { - let dir = tempdir().expect("tempdir"); - let archive_root = dir.path().join("archive"); - fs::create_dir_all(archive_root.join("bin")).expect("archive dir"); - fs::write(archive_root.join("bin/other"), "#!/bin/sh\nexit 0\n").expect("binary"); - let archive_path = dir.path().join("radrootsd.tar.gz"); - let file = File::create(&archive_path).expect("archive file"); - let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); - let mut builder = tar::Builder::new(encoder); - builder - .append_path_with_name(archive_root.join("bin/other"), "radrootsd/bin/other") - .expect("append path"); - builder.finish().expect("finish archive"); - let encoder = builder.into_inner().expect("into encoder"); - encoder.finish().expect("finish gzip"); - - let paths = sample_paths(dir.path()); - let err = extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd")) - .expect_err("archive should not resolve missing binary"); - assert_safe_error( - &err, - "read managed runtime file", - &[ - paths - .install_dir() - .join("radrootsd") - .to_string_lossy() - .as_ref(), - archive_path.to_string_lossy().as_ref(), - ], - ); - } - - #[cfg(unix)] - #[test] - fn extract_binary_archive_reports_unpack_errors() { - let dir = tempdir().expect("tempdir"); - let archive_path = dir.path().join("invalid.tar.gz"); - fs::write(&archive_path, "not a gzip archive").expect("write archive"); - let paths = sample_paths(dir.path()); - - let err = extract_binary_archive(&archive_path, "tar.gz", &paths, &artifact("radrootsd")) - .expect_err("invalid archive should fail"); - assert_safe_error( - &err, - "unpack managed runtime archive", - &[ - archive_path.to_string_lossy().as_ref(), - "not a gzip archive", - ], - ); - } - - #[test] - fn write_instance_config_is_context_bound_and_reports_redacted_errors() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - let config = paths.config_path(); - write_instance_config(&paths, "enabled = true").expect("write config"); - assert_eq!( - fs::read_to_string(&config).expect("read config"), - "enabled = true" - ); - assert!(!paths.secrets_dir().exists()); - - fs::remove_file(&config).expect("remove config"); - fs::create_dir(&config).expect("occupy config path"); - let err = write_instance_config(&paths, "credential = 'secret-value'") - .expect_err("directory write should fail"); - assert_safe_error( - &err, - "write managed runtime config", - &[config.to_string_lossy().as_ref(), "secret-value"], - ); - } - - #[test] - fn start_process_reports_spawn_errors() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - let binary = paths.install_dir().join("unavailable"); - let err = start_process_with( - &binary, - &[], - &[], - &paths, - |_binary, _args, _envs, _stdout, _stderr| { - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "injected spawn denial", - )) - }, - ) - .expect_err("injected spawn failure"); - assert_safe_error( - &err, - "spawn managed runtime process", - &[binary.to_string_lossy().as_ref(), "injected spawn denial"], - ); - } - - #[cfg(unix)] - #[test] - fn start_process_reports_pid_file_write_errors() { - let dir = tempdir().expect("tempdir"); - let binary = dir.path().join("sleepy.sh"); - fs::write(&binary, "#!/bin/sh\nexec sleep 1\n").expect("script"); - let paths = sample_paths(dir.path()); - fs::create_dir_all(paths.pid_file_path()).expect("occupy pid path"); - install_binary(&binary, &sample_paths(dir.path()), &artifact("sleepy.sh")) - .expect("install"); - - let err = start_process(&paths, &artifact("sleepy.sh"), &[], &[]) - .expect_err("pid file write should fail"); - assert_safe_error( - &err, - "write managed runtime pid", - &[paths.pid_file_path().to_string_lossy().as_ref()], - ); - } - - #[test] - fn process_running_and_stop_process_handle_missing_pid_file() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - - assert!(!process_running(&paths).expect("missing pid should be false")); - assert!(!stop_process(&paths).expect("missing pid stop should be false")); - } - - #[test] - fn process_running_reports_invalid_pid_file() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::write(paths.pid_file_path(), "not-a-pid").expect("write pid"); - - let err = process_running(&paths).expect_err("invalid pid should fail"); - assert_safe_error( - &err, - "managed runtime pid is malformed", - &[ - paths.pid_file_path().to_string_lossy().as_ref(), - "not-a-pid", - ], - ); - } - - #[test] - fn stop_process_clears_stale_pid_file() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::write(paths.pid_file_path(), "999999").expect("write pid"); - - assert!(!stop_process(&paths).expect("stale pid should return false")); - assert!(!paths.pid_file_path().exists()); - } - - #[test] - fn stop_process_for_pid_uses_force_kill_after_terminate_attempts() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::write(paths.pid_file_path(), "42").expect("write pid"); - - let mut polls = 0_u32; - let mut is_running = |_pid| { - polls += 1; - polls <= 20 - }; - let mut terminate = ok_runtime_signal; - let mut force_kill = ok_runtime_signal; - let mut sleep = noop_runtime_sleep; - let stopped = stop_process_for_pid( - &paths, - 42, - &mut is_running, - &mut terminate, - &mut force_kill, - &mut sleep, - ) - .expect("force-kill path should stop"); - - assert!(stopped); - assert!(!paths.pid_file_path().exists()); - assert_eq!(polls, 21); - } - - #[test] - fn stop_process_for_pid_stops_after_terminate_poll() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::write(paths.pid_file_path(), "42").expect("write pid"); - - let mut is_running = runtime_is_stopped; - let mut terminate = ok_runtime_signal; - let mut force_kill = ok_runtime_signal; - let mut sleep = noop_runtime_sleep; - let stopped = stop_process_for_pid( - &paths, - 42, - &mut is_running, - &mut terminate, - &mut force_kill, - &mut sleep, - ) - .expect("terminate poll should stop"); - - assert!(stopped); - assert!(!paths.pid_file_path().exists()); - } - - #[test] - fn stop_process_for_pid_reports_failure_after_force_kill_attempts() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::write(paths.pid_file_path(), "42").expect("write pid"); - - let mut sleeps = 0_u32; - let mut is_running = runtime_is_running; - let mut terminate = ok_runtime_signal; - let mut force_kill = ok_runtime_signal; - let mut sleep = |_duration| { - sleeps += 1; - }; - let err = stop_process_for_pid( - &paths, - 42, - &mut is_running, - &mut terminate, - &mut force_kill, - &mut sleep, - ) - .expect_err("force-kill exhaustion should fail"); - - assert_safe_error(&err, "managed runtime process did not stop", &["42"]); - assert_eq!(sleeps, 40); - assert!(paths.pid_file_path().exists()); - } - - #[test] - fn ensure_parent_dir_without_parent_is_a_noop() { - ensure_parent_dir(Path::new("/")).expect("root path should have no parent"); - } - - #[test] - fn ensure_parent_dir_reports_directory_creation_errors() { - let dir = tempdir().expect("tempdir"); - let file_parent = dir.path().join("occupied"); - fs::write(&file_parent, "file").expect("parent file"); - - let err = - ensure_parent_dir(&file_parent.join("child")).expect_err("file parent should fail"); - assert_safe_error( - &err, - "create managed runtime directory", - &[file_parent.to_string_lossy().as_ref()], - ); - } - - #[test] - fn find_binary_with_name_handles_nested_and_missing_files() { - let dir = tempdir().expect("tempdir"); - fs::create_dir_all(dir.path().join("nested")).expect("nested dir"); - fs::write(dir.path().join("nested/radrootsd"), "binary").expect("binary"); - - assert_eq!( - find_binary_with_name(dir.path(), "radrootsd"), - Some(dir.path().join("nested/radrootsd")) - ); - assert_eq!(find_binary_with_name(dir.path(), "missing"), None); - } - - #[test] - fn open_log_file_creates_file_and_reports_directory_errors() { - let dir = tempdir().expect("tempdir"); - let file_path = dir.path().join("logs/stdout.log"); - let file = open_log_file(&file_path).expect("open log"); - drop(file); - assert!(file_path.is_file()); - - let bad_path = dir.path().join("bad"); - fs::create_dir(&bad_path).expect("create dir"); - let err = open_log_file(&bad_path).expect_err("directory open should fail"); - assert_safe_error( - &err, - "open managed runtime log", - &[bad_path.to_string_lossy().as_ref()], - ); - } - - #[test] - fn read_pid_handles_empty_missing_and_read_error_cases() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - - assert_eq!(read_pid(&paths).expect("missing pid"), None); - - ensure_instance_layout(&paths).expect("layout"); - fs::write(paths.pid_file_path(), " ").expect("write pid"); - assert_eq!(read_pid(&paths).expect("empty pid"), None); - - fs::remove_file(paths.pid_file_path()).expect("remove pid file"); - fs::create_dir(paths.pid_file_path()).expect("occupy pid path"); - let err = read_pid(&paths).expect_err("directory pid file should fail"); - assert_safe_error( - &err, - "read managed runtime pid", - &[paths.pid_file_path().to_string_lossy().as_ref()], - ); - } - - #[test] - fn remove_path_if_exists_handles_files_directories_and_missing_paths() { - let dir = tempdir().expect("tempdir"); - let file_path = dir.path().join("file.txt"); - let dir_path = dir.path().join("subdir"); - fs::write(&file_path, "data").expect("file"); - fs::create_dir(&dir_path).expect("dir"); - - remove_path_if_exists(&file_path).expect("remove file"); - remove_path_if_exists(&dir_path).expect("remove dir"); - remove_path_if_exists(dir.path().join("missing").as_path()).expect("remove missing"); - - assert!(!file_path.exists()); - assert!(!dir_path.exists()); - } - - #[test] - fn remove_path_from_state_reports_dir_file_and_metadata_errors() { - let dir = tempdir().expect("tempdir"); - let dir_path = dir.path().join("subdir"); - let file_path = dir.path().join("file.txt"); - let metadata_path = dir.path().join("metadata"); - ok_remove_path(Path::new("/")).expect("noop remove path"); - - let dir_err = remove_path_from_state( - &dir_path, - Ok(Some(ExistingPathKind::Directory)), - deny_remove_path, - ok_remove_path, - ) - .expect_err("directory removal should fail"); - assert_safe_error( - &dir_err, - "remove manager-owned runtime path", - &[dir_path.to_string_lossy().as_ref(), "remove path denied"], - ); - - let file_err = remove_path_from_state( - &file_path, - Ok(Some(ExistingPathKind::File)), - ok_remove_path, - deny_remove_path, - ) - .expect_err("file removal should fail"); - assert_safe_error( - &file_err, - "remove manager-owned runtime path", - &[file_path.to_string_lossy().as_ref(), "remove path denied"], - ); - - let metadata_err = remove_path_from_state( - &metadata_path, - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "metadata lookup failed", - )), - ok_remove_path, - ok_remove_path, - ) - .expect_err("metadata lookup should fail"); - assert_safe_error( - &metadata_err, - "read managed runtime file", - &[ - metadata_path.to_string_lossy().as_ref(), - "metadata lookup failed", - ], - ); - } - - #[test] - fn remove_pid_file_reports_directory_errors() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - ensure_instance_layout(&paths).expect("layout"); - fs::create_dir(paths.pid_file_path()).expect("occupy pid path"); - - let err = super::remove_pid_file(&paths).expect_err("directory pid path should fail"); - assert_safe_error( - &err, - "remove manager-owned runtime path", - &[paths.pid_file_path().to_string_lossy().as_ref()], - ); - } - - #[test] - fn remove_pid_file_accepts_missing_pid_paths() { - let dir = tempdir().expect("tempdir"); - let paths = sample_paths(dir.path()); - super::remove_pid_file(&paths).expect("missing pid file should be ignored"); - } - - #[cfg(unix)] - #[test] - fn executable_mode_reports_missing_path_errors() { - let dir = tempdir().expect("tempdir"); - let missing = dir.path().join("missing"); - - let err = set_executable_mode(&missing).expect_err("missing executable should fail"); - assert_safe_error( - &err, - "read managed runtime file", - &[missing.to_string_lossy().as_ref()], - ); - } - - #[cfg(unix)] - #[test] - fn apply_mode_reports_set_permissions_errors() { - let dir = tempdir().expect("tempdir"); - let path = dir.path().join("radrootsd"); - fs::write(&path, "binary").expect("binary"); - - let err = apply_mode(&path, 0o755, |_path, _permissions| { - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "set permissions failed", - )) - }) - .expect_err("set permissions should fail"); - assert_safe_error( - &err, - "set managed runtime file permissions", - &[path.to_string_lossy().as_ref(), "set permissions failed"], - ); - } - - #[cfg(unix)] - #[test] - fn signal_helpers_cover_failure_paths() { - let missing_pid = 999_999_u32; - assert!(!process_running_for_pid(missing_pid)); - - let err = terminate_process(missing_pid).expect_err("terminate should fail"); - assert_safe_error(&err, "managed runtime process did not stop", &["999999"]); - - let err = force_kill_process(missing_pid).expect_err("force kill should fail"); - assert_safe_error(&err, "managed runtime process did not stop", &["999999"]); - - let err = signal_process(missing_pid, "-BOGUS").expect_err("invalid signal should fail"); - assert_safe_error(&err, "managed runtime process did not stop", &["999999"]); - } - - #[cfg(unix)] - #[test] - fn signal_process_with_reports_execution_errors() { - let err = signal_process_with(42, "-TERM", |_pid, _signal| { - Err(io::Error::new( - io::ErrorKind::NotFound, - "kill executable missing", - )) - }) - .expect_err("signal execution should fail"); - assert_safe_error( - &err, - "signal managed runtime process", - &["42", "-TERM", "kill executable missing"], - ); - } - - #[cfg(unix)] - #[test] - fn process_running_state_from_ps_output_handles_non_success_and_zombies() { - assert!(process_running_state_from_ps_output(output_with_status( - exit_status(1), - b"", - ))); - assert!(!process_running_state_from_ps_output(output_with_status( - exit_status(0), - b"Z+", - ))); - assert!(process_running_state_from_ps_output(output_with_status( - exit_status(0), - b"S+", - ))); - } -} diff --git a/crates/runtime_manager/src/managed.rs b/crates/runtime_manager/src/managed.rs @@ -1,67 +1,61 @@ use core::fmt; -use std::path::Path; use radroots_runtime_distribution::HardenedServiceTarget; -use radroots_runtime_paths::{RadrootsPathProfile, RuntimeContext, RuntimeContextSource}; +use radroots_runtime_paths::{InstanceId, RadrootsPathProfile, ServiceId}; -use crate::paths::resolve_shared_paths; use crate::{ - BootstrapRuntimeContract, ManagedRuntimeHealthState, ManagedRuntimeInstallState, - ManagedRuntimeInstancePaths, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, ManagementModeContract, RadrootsRuntimeManagementContract, RadrootsRuntimeManagerError, - load_registry, }; +/// Validated metadata-only runtime-management context. +/// +/// The context owns no filesystem path, registry, process, artifact, or +/// lifecycle capability. Its fields are private so a caller cannot bypass the +/// exact contract validation performed by [`ManagedRuntimeContext::new`]. +/// +/// ```compile_fail +/// use radroots_runtime_manager::ManagedRuntimeContext; +/// +/// let _ = ManagedRuntimeContext { +/// contract: todo!(), +/// profile: todo!(), +/// management_mode: String::new(), +/// }; +/// ``` #[derive(Clone)] pub struct ManagedRuntimeContext { contract: RadrootsRuntimeManagementContract, - manager_context: RuntimeContext, - shared_paths: crate::ManagedRuntimeSharedPaths, - registry: ManagedRuntimeInstanceRegistry, + profile: RadrootsPathProfile, + management_mode: String, } impl ManagedRuntimeContext { - #[must_use] - pub fn contract(&self) -> &RadrootsRuntimeManagementContract { - &self.contract + pub fn new( + contract: RadrootsRuntimeManagementContract, + profile: RadrootsPathProfile, + ) -> Result<Self, RadrootsRuntimeManagerError> { + crate::validate_hardened_management_contract(&contract)?; + let management_mode = active_management_mode_for_profile(&contract, profile)?.to_owned(); + Ok(Self { + contract, + profile, + management_mode, + }) } #[must_use] - pub fn manager_context(&self) -> &RuntimeContext { - &self.manager_context + pub fn contract(&self) -> &RadrootsRuntimeManagementContract { + &self.contract } #[must_use] - pub fn shared_paths(&self) -> &crate::ManagedRuntimeSharedPaths { - &self.shared_paths + pub const fn profile(&self) -> RadrootsPathProfile { + self.profile } #[must_use] - pub fn registry(&self) -> &ManagedRuntimeInstanceRegistry { - &self.registry - } - - pub fn register_instance( - &mut self, - runtime_context: &RuntimeContext, - _install_state: ManagedRuntimeInstallState, - ) -> Result<(), RadrootsRuntimeManagerError> { - ensure_context_scope(&self.manager_context, runtime_context)?; - match self.contract.service_targets.get(runtime_context.service()) { - Some(_) => Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget), - None => Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget), - } - } - - pub fn remove_instance( - &mut self, - runtime_context: &RuntimeContext, - ) -> Result<Option<ManagedRuntimeInstanceRecord>, RadrootsRuntimeManagerError> { - ensure_context_scope(&self.manager_context, runtime_context)?; - match self.contract.service_targets.get(runtime_context.service()) { - Some(_) => Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget), - None => Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget), - } + pub fn management_mode(&self) -> &str { + &self.management_mode } } @@ -69,86 +63,53 @@ impl fmt::Debug for ManagedRuntimeContext { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("ManagedRuntimeContext") - .field("manager_context", &self.manager_context) - .field("shared_paths", &self.shared_paths) - .field("registry", &"[redacted]") + .field("profile", &self.profile) + .field("management_mode", &self.management_mode) .finish_non_exhaustive() } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ManagedRuntimeGroup { - ActiveManagedTarget, - DefinedManagedTarget, - BootstrapOnly, - Unknown, -} - -impl ManagedRuntimeGroup { - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::ActiveManagedTarget => "active_managed_target", - Self::DefinedManagedTarget => "defined_managed_target", - Self::BootstrapOnly => "bootstrap_only", - Self::Unknown => "unknown", - } - } - - #[must_use] - pub fn posture(self) -> &'static str { - match self { - Self::ActiveManagedTarget => "active_managed_target", - Self::DefinedManagedTarget => "defined_future_target", - Self::BootstrapOnly => "bootstrap_only_direct_binding", - Self::Unknown => "unknown_runtime", - } - } -} - -/// A sealed, internally cross-bound management target. +/// Sealed metadata for one explicitly selected Myc or RHI instance. +/// +/// The target deliberately carries no resolved runtime paths and exposes no +/// lifecycle, registry, filesystem, process, or artifact capability. /// /// ```compile_fail /// use radroots_runtime_manager::ManagedRuntimeTarget; /// /// let _ = ManagedRuntimeTarget { -/// context: todo!(), -/// instance_source: todo!(), -/// runtime_group: todo!(), -/// management_mode: None, -/// mode_contract: None, -/// bootstrap: None, -/// instance_record: None, -/// predicted_paths: None, +/// service_id: todo!(), +/// instance_id: todo!(), +/// profile: todo!(), +/// service_target: todo!(), +/// management_mode: String::new(), +/// mode_contract: todo!(), /// }; /// ``` #[derive(Clone)] pub struct ManagedRuntimeTarget { - context: RuntimeContext, - instance_source: RuntimeContextSource, - runtime_group: ManagedRuntimeGroup, + service_id: ServiceId, + instance_id: InstanceId, + profile: RadrootsPathProfile, service_target: HardenedServiceTarget, - management_mode: Option<String>, - mode_contract: Option<ManagementModeContract>, - bootstrap: Option<BootstrapRuntimeContract>, - instance_record: Option<ManagedRuntimeInstanceRecord>, - predicted_paths: Option<ManagedRuntimeInstancePaths>, + management_mode: String, + mode_contract: ManagementModeContract, } impl ManagedRuntimeTarget { #[must_use] - pub fn context(&self) -> &RuntimeContext { - &self.context + pub fn service_id(&self) -> &ServiceId { + &self.service_id } #[must_use] - pub fn instance_source(&self) -> RuntimeContextSource { - self.instance_source + pub fn instance_id(&self) -> &InstanceId { + &self.instance_id } #[must_use] - pub fn runtime_group(&self) -> ManagedRuntimeGroup { - self.runtime_group + pub const fn profile(&self) -> RadrootsPathProfile { + self.profile } #[must_use] @@ -157,28 +118,13 @@ impl ManagedRuntimeTarget { } #[must_use] - pub fn management_mode(&self) -> Option<&str> { - self.management_mode.as_deref() - } - - #[must_use] - pub fn mode_contract(&self) -> Option<&ManagementModeContract> { - self.mode_contract.as_ref() + pub fn management_mode(&self) -> &str { + &self.management_mode } #[must_use] - pub fn bootstrap(&self) -> Option<&BootstrapRuntimeContract> { - self.bootstrap.as_ref() - } - - #[must_use] - pub fn instance_record(&self) -> Option<&ManagedRuntimeInstanceRecord> { - self.instance_record.as_ref() - } - - #[must_use] - pub fn predicted_paths(&self) -> Option<&ManagedRuntimeInstancePaths> { - self.predicted_paths.as_ref() + pub fn mode_contract(&self) -> &ManagementModeContract { + &self.mode_contract } } @@ -186,131 +132,43 @@ impl fmt::Debug for ManagedRuntimeTarget { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("ManagedRuntimeTarget") - .field("context", &self.context) - .field("runtime_group", &self.runtime_group) - .field("service_target", &self.service_target) - .field("predicted_paths", &self.predicted_paths) + .field("service_id", &self.service_id) + .field("instance_id", &self.instance_id) + .field("profile", &self.profile) + .field("management_mode", &self.management_mode) .finish_non_exhaustive() } } -#[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 { - #[must_use] - 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: RuntimeContextSource, - 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 lifecycle_actions: Vec<String>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ManagedRuntimeLogsInspection { - pub runtime_id: String, - pub instance_id: String, - pub instance_source: RuntimeContextSource, - pub runtime_group: String, - pub state: String, - pub source: String, - pub detail: String, - 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: RuntimeContextSource, - pub runtime_group: String, - pub state: String, - pub source: String, - pub detail: String, - pub config_format: Option<String>, - 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: RuntimeContextSource, - pub runtime_group: String, - pub state: String, - pub source: String, - pub detail: String, - pub mutates_bindings: bool, - pub next_step: Option<String>, -} +pub fn resolve_runtime_target( + context: &ManagedRuntimeContext, + service_id: ServiceId, + instance_id: InstanceId, +) -> Result<ManagedRuntimeTarget, RadrootsRuntimeManagerError> { + let service_target = context + .contract + .service_targets + .get(&service_id) + .cloned() + .ok_or(RadrootsRuntimeManagerError::UnsupportedServiceTarget)?; + let mode_contract = context + .contract + .mode + .get(&context.management_mode) + .cloned() + .ok_or(RadrootsRuntimeManagerError::InvalidContract)?; -pub fn load_management_context( - contract: RadrootsRuntimeManagementContract, - manager_context: RuntimeContext, -) -> Result<ManagedRuntimeContext, RadrootsRuntimeManagerError> { - crate::validate_hardened_management_contract(&contract)?; - active_management_mode_for_profile(&contract, manager_context.profile())?; - let shared_paths = resolve_shared_paths(&manager_context); - let registry = load_registry(shared_paths.instance_registry_path())?; - Ok(ManagedRuntimeContext { - contract, - manager_context, - shared_paths, - registry, + Ok(ManagedRuntimeTarget { + service_id, + instance_id, + profile: context.profile, + service_target, + management_mode: context.management_mode.clone(), + mode_contract, }) } -pub fn active_management_mode_for_profile( +fn active_management_mode_for_profile( contract: &RadrootsRuntimeManagementContract, profile: RadrootsPathProfile, ) -> Result<&str, RadrootsRuntimeManagerError> { @@ -329,892 +187,131 @@ pub fn active_management_mode_for_profile( .ok_or(RadrootsRuntimeManagerError::UnsupportedProfile) } -pub fn resolve_runtime_target( - context: &ManagedRuntimeContext, - runtime_context: RuntimeContext, -) -> Result<ManagedRuntimeTarget, RadrootsRuntimeManagerError> { - ensure_context_scope(&context.manager_context, &runtime_context)?; - let runtime_id = runtime_context.service().as_str(); - let runtime_group = runtime_group(&context.contract, runtime_id); - let service_target = context - .contract - .service_targets - .get(runtime_context.service()) - .cloned() - .ok_or(RadrootsRuntimeManagerError::UnsupportedServiceTarget)?; - let bootstrap = context.contract.bootstrap.get(runtime_id).cloned(); - let management_mode = Some( - active_management_mode_for_profile(&context.contract, runtime_context.profile())? - .to_owned(), - ); - let mode_contract = management_mode - .as_ref() - .and_then(|mode_id| context.contract.mode.get(mode_id).cloned()); - let instance_record = None; - let predicted_paths = None; - - Ok(ManagedRuntimeTarget { - instance_source: runtime_context.sources().instance(), - context: runtime_context, - runtime_group, - service_target, - management_mode, - mode_contract, - bootstrap, - instance_record, - predicted_paths, - }) -} - -fn ensure_context_scope( - manager: &RuntimeContext, - target: &RuntimeContext, -) -> Result<(), RadrootsRuntimeManagerError> { - if manager.profile() != target.profile() - || context_roots(manager) - .iter() - .zip(context_roots(target)) - .any(|(left, right)| left != &right) - { - return Err(RadrootsRuntimeManagerError::RuntimeContextMismatch); - } - Ok(()) -} - -fn context_roots(context: &RuntimeContext) -> [&Path; 6] { - let paths = context.paths(); - [ - instance_root(paths.config()), - instance_root(paths.state()), - instance_root(paths.cache()), - instance_root(paths.logs()), - instance_root(paths.run()), - instance_root(paths.secrets()), - ] -} - -fn instance_root(path: &Path) -> &Path { - path.parent() - .and_then(Path::parent) - .and_then(Path::parent) - .expect("RuntimeContext service paths contain the sealed services/service/instance suffix") -} - -#[must_use] -pub fn inspect_runtime_status( - target: &ManagedRuntimeTarget, - lifecycle_actions: &[String], -) -> ManagedRuntimeInspection<ManagedRuntimeStatusInspection> { - let availability = managed_inspection_availability(target); - let (health_state, health_source) = infer_health_state(target); - - ManagedRuntimeInspection { - availability, - view: ManagedRuntimeStatusInspection { - runtime_id: target.context.service().to_string(), - instance_id: target.context.instance().to_string(), - instance_source: target.instance_source, - 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 + typed 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(BootstrapRuntimeContract::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: health_state.to_owned(), - health_source: health_source.to_owned(), - lifecycle_actions: if target.runtime_group == ManagedRuntimeGroup::ActiveManagedTarget { - lifecycle_actions.to_vec() - } else { - Vec::new() - }, - }, - } -} - -#[must_use] -pub fn inspect_runtime_logs( - target: &ManagedRuntimeTarget, -) -> ManagedRuntimeInspection<ManagedRuntimeLogsInspection> { - let availability = managed_inspection_availability(target); - let stdout_log_present = (availability == ManagedRuntimeInspectionAvailability::Success) - && target - .predicted_paths - .as_ref() - .is_some_and(|paths| paths.stdout_log_path().exists()); - let stderr_log_present = (availability == ManagedRuntimeInspectionAvailability::Success) - && target - .predicted_paths - .as_ref() - .is_some_and(|paths| paths.stderr_log_path().exists()); - - ManagedRuntimeInspection { - availability, - view: ManagedRuntimeLogsInspection { - runtime_id: target.context.service().to_string(), - instance_id: target.context.instance().to_string(), - instance_source: target.instance_source, - runtime_group: target.runtime_group.as_str().to_owned(), - state: availability_state(availability), - source: "runtime management contract + manager-owned tracking".to_owned(), - detail: logs_detail(target), - stdout_log_present, - stderr_log_present, - }, - } -} - -#[must_use] -pub fn inspect_runtime_config( - target: &ManagedRuntimeTarget, -) -> ManagedRuntimeInspection<ManagedRuntimeConfigInspection> { - let availability = managed_inspection_availability(target); - let config_path = (availability == ManagedRuntimeInspectionAvailability::Success) - .then_some(()) - .and(target.instance_record.as_ref()) - .and_then(|_| { - target - .predicted_paths - .as_ref() - .map(ManagedRuntimeInstancePaths::config_path) - }); - let config_present = config_path.as_deref().is_some_and(Path::exists); - - ManagedRuntimeInspection { - availability, - view: ManagedRuntimeConfigInspection { - runtime_id: target.context.service().to_string(), - instance_id: target.context.instance().to_string(), - instance_source: target.instance_source, - runtime_group: target.runtime_group.as_str().to_owned(), - state: match availability { - ManagedRuntimeInspectionAvailability::Success if config_path.is_some() => { - "ready".to_owned() - } - ManagedRuntimeInspectionAvailability::Success => "not_installed".to_owned(), - other => availability_state(other), - }, - source: "runtime context + typed instance registry".to_owned(), - detail: config_detail(target, config_path.is_some()), - config_format: Some(target.service_target.config_format().as_str().to_owned()), - config_present, - requires_bootstrap_secret: None, - requires_config_bootstrap: Some(true), - requires_signer_provider: None, - }, - } -} - -#[must_use] -pub fn inspect_runtime_action( - target: &ManagedRuntimeTarget, - action: ManagedRuntimeLifecycleAction, -) -> ManagedRuntimeInspection<ManagedRuntimeActionInspection> { - let (availability, state, detail) = match target.runtime_group { - ManagedRuntimeGroup::ActiveManagedTarget => ( - ManagedRuntimeInspectionAvailability::Unsupported, - "deferred", - format!( - "runtime {} `{}` is not supported for this managed target", - action.as_str().replace('_', " "), - target.context.service() - ), - ), - ManagedRuntimeGroup::DefinedManagedTarget => ( - ManagedRuntimeInspectionAvailability::Unsupported, - "unsupported", - format!( - "runtime `{}` is only a defined future managed target; `{}` is not admitted in the current wave", - target.context.service(), - action.as_str().replace('_', " ") - ), - ), - ManagedRuntimeGroup::BootstrapOnly => ( - ManagedRuntimeInspectionAvailability::Unsupported, - "unsupported", - format!( - "runtime `{}` remains bootstrap_only; generic managed `{}` is not admitted", - target.context.service(), - action.as_str().replace('_', " ") - ), - ), - ManagedRuntimeGroup::Unknown => ( - ManagedRuntimeInspectionAvailability::Unconfigured, - "unknown_runtime", - unknown_runtime_detail(target), - ), - }; - - ManagedRuntimeInspection { - availability, - view: ManagedRuntimeActionInspection { - action: action.as_str().to_owned(), - runtime_id: target.context.service().to_string(), - instance_id: target.context.instance().to_string(), - instance_source: target.instance_source, - 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: None, - }, - } -} - -fn managed_inspection_availability( - target: &ManagedRuntimeTarget, -) -> ManagedRuntimeInspectionAvailability { - 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 - } - } - } -} - -fn availability_state(availability: ManagedRuntimeInspectionAvailability) -> String { - match availability { - ManagedRuntimeInspectionAvailability::Success => "ready", - ManagedRuntimeInspectionAvailability::Unconfigured => "unknown_runtime", - ManagedRuntimeInspectionAvailability::Unsupported => "unsupported", - } - .to_owned() -} - -fn status_state(target: &ManagedRuntimeTarget) -> &'static str { - match target.runtime_group { - ManagedRuntimeGroup::ActiveManagedTarget => target - .instance_record - .as_ref() - .map(|record| install_state_label(record.install_state())) - .unwrap_or("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 if target.instance_record.is_some() => format!( - "managed runtime `{}` instance `{}` is registered", - target.context.service(), - target.context.instance() - ), - ManagedRuntimeGroup::ActiveManagedTarget => format!( - "managed runtime `{}` has no registered instance `{}`", - target.context.service(), - target.context.instance() - ), - ManagedRuntimeGroup::DefinedManagedTarget => format!( - "runtime `{}` is defined but not yet an active managed target", - target.context.service() - ), - ManagedRuntimeGroup::BootstrapOnly => format!( - "runtime `{}` is bootstrap_only in the management contract", - target.context.service() - ), - ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target), - } -} - -fn logs_detail(target: &ManagedRuntimeTarget) -> String { - match target.runtime_group { - ManagedRuntimeGroup::ActiveManagedTarget => { - "runtime logs use manager-owned stdout/stderr tracking".to_owned() - } - ManagedRuntimeGroup::DefinedManagedTarget => format!( - "runtime `{}` is a defined future managed target", - target.context.service() - ), - ManagedRuntimeGroup::BootstrapOnly => format!( - "runtime `{}` is bootstrap_only; generic managed logs are not admitted", - target.context.service() - ), - ManagedRuntimeGroup::Unknown => unknown_runtime_detail(target), - } -} - -fn config_detail(target: &ManagedRuntimeTarget, registered: bool) -> String { - match target.runtime_group { - ManagedRuntimeGroup::ActiveManagedTarget if registered => { - "runtime config is derived from the validated service context".to_owned() - } - ManagedRuntimeGroup::ActiveManagedTarget => format!( - "managed runtime `{}` has no registered instance config", - target.context.service() - ), - ManagedRuntimeGroup::DefinedManagedTarget => format!( - "runtime `{}` is a defined future managed target", - target.context.service() - ), - ManagedRuntimeGroup::BootstrapOnly => format!( - "runtime `{}` is bootstrap_only; generic managed config is not admitted", - target.context.service() - ), - 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.context.service() - ) -} - -fn infer_health_state(target: &ManagedRuntimeTarget) -> (&'static str, &'static str) { - if target.runtime_group != ManagedRuntimeGroup::ActiveManagedTarget { - return ( - health_state_label(ManagedRuntimeHealthState::NotInstalled), - "metadata_only", - ); - } - 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 target - .predicted_paths - .as_ref() - .is_some_and(|paths| crate::process_running(paths).unwrap_or(false)) - { - return ( - health_state_label(ManagedRuntimeHealthState::Running), - "process_probe", - ); - } - if record.install_state() == ManagedRuntimeInstallState::NotInstalled { - ( - health_state_label(ManagedRuntimeHealthState::NotInstalled), - "registry_install_state", - ) - } else { - ( - health_state_label(ManagedRuntimeHealthState::Stopped), - "pid_file_absent", - ) - } -} - -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", - } -} - -#[must_use] -pub fn runtime_group( - contract: &RadrootsRuntimeManagementContract, - runtime_id: &str, -) -> ManagedRuntimeGroup { - if contract - .managed_runtime_targets - .active - .iter() - .any(|entry| entry == runtime_id) - { - ManagedRuntimeGroup::ActiveManagedTarget - } else if contract - .managed_runtime_targets - .defined - .iter() - .any(|entry| entry == runtime_id) - { - ManagedRuntimeGroup::DefinedManagedTarget - } else if contract - .managed_runtime_targets - .bootstrap_only - .iter() - .any(|entry| entry == runtime_id) - { - ManagedRuntimeGroup::BootstrapOnly - } else { - ManagedRuntimeGroup::Unknown - } -} - #[cfg(test)] mod tests { - use std::fs; - - use radroots_runtime_paths::{ - InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, - }; - use tempfile::tempdir; + use radroots_runtime_paths::{InstanceId, RadrootsPathProfile, ServiceId}; - use super::{ - ManagedRuntimeGroup, ManagedRuntimeInspectionAvailability, ManagedRuntimeLifecycleAction, - active_management_mode_for_profile, inspect_runtime_action, inspect_runtime_config, - inspect_runtime_logs, inspect_runtime_status, load_management_context, - resolve_runtime_target, runtime_group, - }; - use crate::paths::resolve_instance_paths; - use crate::{ - HARDENED_MANAGEMENT_CONTRACT, ManagedRuntimeHealthState, ManagedRuntimeInstallState, - ManagedRuntimeInstanceRecord, RadrootsRuntimeManagerError, parse_contract_str, - }; + use super::{ManagedRuntimeContext, resolve_runtime_target}; + use crate::{HARDENED_MANAGEMENT_CONTRACT, RadrootsRuntimeManagerError, parse_contract_str}; - const CONTRACT: &str = HARDENED_MANAGEMENT_CONTRACT; - - fn context(service: &str, instance: &str, root: &std::path::Path) -> RuntimeContext { - RuntimeContext::resolve( - &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), - RuntimeContextBootstrap::new( - RadrootsPathProfile::RepoLocal, - Some(root.to_path_buf()), - RuntimeContextSource::BootstrapCli, - RuntimeContextSource::BootstrapCli, - ) - .expect("bootstrap"), - ServiceId::new(service).expect("service"), - InstanceId::new(instance).expect("instance"), + fn context(profile: RadrootsPathProfile) -> ManagedRuntimeContext { + ManagedRuntimeContext::new( + parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"), + profile, ) - .expect("context") + .expect("management context") } - fn manager(root: &std::path::Path) -> super::ManagedRuntimeContext { - load_management_context( - parse_contract_str(CONTRACT).expect("contract"), - context("runtime-manager", "default", root), + fn target( + context: &ManagedRuntimeContext, + service: &str, + instance: &str, + ) -> super::ManagedRuntimeTarget { + resolve_runtime_target( + context, + ServiceId::new(service).expect("service"), + InstanceId::new(instance).expect("instance"), ) - .expect("manager") - } - - #[test] - fn manager_loads_from_its_context_and_rejects_inactive_profiles() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - assert_eq!( - manager.manager_context().service().as_str(), - "runtime-manager" - ); - assert!(manager.registry().instances.is_empty()); - assert!( - manager - .shared_paths() - .instance_registry_path() - .ends_with("services/runtime-manager/default/instances.toml") - ); - - let contract = parse_contract_str(CONTRACT).expect("contract"); - assert_eq!( - active_management_mode_for_profile(&contract, RadrootsPathProfile::RepoLocal) - .expect("active mode"), - "interactive_user_managed" - ); - assert_eq!( - active_management_mode_for_profile(&contract, RadrootsPathProfile::ServiceHost) - .expect("service-host mode"), - "service_host_managed" - ); + .expect("target") } #[test] - fn targets_bind_exact_typed_contexts_and_multi_instance_records() { - let dir = tempdir().expect("tempdir"); - let mut manager = manager(dir.path()); - let primary = context("myc", "primary", dir.path()); - let secondary = context("myc", "secondary", dir.path()); - - let primary_target = resolve_runtime_target(&manager, primary.clone()).expect("primary"); - let secondary_target = - resolve_runtime_target(&manager, secondary.clone()).expect("secondary"); - assert!(primary_target.instance_record.is_none()); - assert!(secondary_target.instance_record.is_none()); - assert_eq!(primary_target.context, primary); - assert_eq!(secondary_target.context, secondary); - assert_ne!( - primary_target.context.paths(), - secondary_target.context.paths() - ); - assert_eq!( - primary_target.runtime_group, - ManagedRuntimeGroup::DefinedManagedTarget - ); - assert!(primary_target.predicted_paths.is_none()); - assert_eq!(primary_target.service_target().service_id().as_str(), "myc"); - assert_eq!( - manager.register_instance(&primary, ManagedRuntimeInstallState::Configured), - Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget) - ); - assert_eq!( - manager.remove_instance(&primary), - Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget) - ); - } + fn contexts_accept_only_exact_contracts_and_supported_profiles() { + for (profile, mode) in [ + (RadrootsPathProfile::RepoLocal, "interactive_user_managed"), + (RadrootsPathProfile::ServiceHost, "service_host_managed"), + ] { + let context = context(profile); + assert_eq!(context.profile(), profile); + assert_eq!(context.management_mode(), mode); + assert_eq!(context.contract().service_targets.len(), 2); + } - #[test] - fn manager_rejects_same_identity_from_a_different_root_scope() { - let first = tempdir().expect("first"); - let second = tempdir().expect("second"); - let mut manager = manager(first.path()); - let mismatched = context("myc", "primary", second.path()); + for profile in [ + RadrootsPathProfile::InteractiveUser, + RadrootsPathProfile::MobileNative, + ] { + let contract = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); + assert!(matches!( + ManagedRuntimeContext::new(contract, profile), + Err(RadrootsRuntimeManagerError::UnsupportedProfile) + )); + } + let mut direct = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); + direct.lifecycle.actions.push("start".to_owned()); assert!(matches!( - manager.register_instance(&mismatched, ManagedRuntimeInstallState::Configured), - Err(RadrootsRuntimeManagerError::RuntimeContextMismatch) + ManagedRuntimeContext::new(direct, RadrootsPathProfile::RepoLocal), + Err(RadrootsRuntimeManagerError::InvalidContract) )); - assert!(matches!( - resolve_runtime_target(&manager, mismatched), - Err(RadrootsRuntimeManagerError::RuntimeContextMismatch) - )); - assert!(manager.registry().instances().is_empty()); - } - - #[test] - fn groups_and_unknown_targets_remain_contract_controlled() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - let contract = manager.contract(); - assert_eq!( - runtime_group(contract, "radrootsd"), - ManagedRuntimeGroup::Unknown - ); - assert_eq!( - runtime_group(contract, "myc"), - ManagedRuntimeGroup::DefinedManagedTarget - ); - assert_eq!(runtime_group(contract, "hyf"), ManagedRuntimeGroup::Unknown); - assert_eq!( - runtime_group(contract, "unknown"), - ManagedRuntimeGroup::Unknown - ); - - assert_eq!( - resolve_runtime_target(&manager, context("unknown", "default", dir.path())) - .expect_err("unknown target"), - RadrootsRuntimeManagerError::UnsupportedServiceTarget - ); - } - - #[test] - fn status_is_metadata_only_without_probing_manager_tracking() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - let service = context("myc", "primary", dir.path()); - let target = resolve_runtime_target(&manager, service).expect("target"); - assert!(target.predicted_paths().is_none()); - - let status = inspect_runtime_status(&target, &["start".to_owned()]); - assert_eq!( - status.availability, - ManagedRuntimeInspectionAvailability::Unsupported - ); - assert_eq!(status.view.health_state, "not_installed"); - assert_eq!(status.view.health_source, "metadata_only"); - assert!(status.view.lifecycle_actions.is_empty()); - assert_eq!( - status.view.instance_source, - RuntimeContextSource::BootstrapCli - ); - let rendered = format!("{status:?}"); - assert!(!rendered.contains(dir.path().to_string_lossy().as_ref())); - assert!(!status.view.detail.contains('/')); - } - - #[test] - fn log_and_config_inspections_remain_non_io_for_metadata_only_services() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - let service = context("rhi", "default", dir.path()); - let target = resolve_runtime_target(&manager, service).expect("target"); - assert!(target.predicted_paths().is_none()); - fs::create_dir_all(target.context().paths().logs()).expect("service logs"); - fs::write(target.context().paths().logs().join("stdout.log"), "stdout") - .expect("service stdout"); - fs::create_dir_all(target.context().paths().config()).expect("service config"); - fs::write( - target.context().paths().config().join("config.toml"), - "enabled = true", - ) - .expect("service config"); - - let logs = inspect_runtime_logs(&target); - assert_eq!( - logs.availability, - ManagedRuntimeInspectionAvailability::Unsupported - ); - assert!(!logs.view.stdout_log_present); - assert!(!logs.view.stderr_log_present); - let config = inspect_runtime_config(&target); - assert_eq!( - config.availability, - ManagedRuntimeInspectionAvailability::Unsupported - ); - assert!(!config.view.config_present); - assert_eq!(config.view.config_format.as_deref(), Some("toml")); - for rendered in [format!("{logs:?}"), format!("{config:?}")] { - assert!(!rendered.contains(dir.path().to_string_lossy().as_ref())); - } - } - - #[test] - fn actions_do_not_mutate_bindings_for_any_group() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - for service in ["myc", "rhi"] { - let target = resolve_runtime_target(&manager, context(service, "default", dir.path())) - .expect("target"); - let action = inspect_runtime_action(&target, ManagedRuntimeLifecycleAction::ConfigSet); - assert_eq!( - action.availability, - ManagedRuntimeInspectionAvailability::Unsupported - ); - assert!(!action.view.mutates_bindings); - assert!(action.view.next_step.is_none()); - } } #[test] - fn durable_management_contract_requires_explicit_instances_and_rejects_any_drift() { - let contract = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); - assert_eq!(contract.service_targets.len(), 2); - assert!(contract.bootstrap.is_empty()); - - for raw in [ - HARDENED_MANAGEMENT_CONTRACT.replace("schema_version = 1", "schema_version = 2"), - HARDENED_MANAGEMENT_CONTRACT.replace( - "defined = [\"myc\", \"rhi\"]", - "active = [\"myc\"]\ndefined = [\"rhi\"]", - ), - HARDENED_MANAGEMENT_CONTRACT.replace( - "managed_runtime_lookup = \"typed_instance_registry\"", - "managed_runtime_lookup = \"different\"", - ), - HARDENED_MANAGEMENT_CONTRACT.replace("active = [\"cli\"]", "active = [\"other\"]"), - HARDENED_MANAGEMENT_CONTRACT.replace( - "supported_profiles = [\"interactive\", \"repo_local\"]", - "supported_profiles = [\"interactive\"]", - ), - HARDENED_MANAGEMENT_CONTRACT.replace( - "required_fields = [\"service_id\", \"instance_id\"]", - "required_fields = [\"service_id\"]", - ), - HARDENED_MANAGEMENT_CONTRACT.replace( - "distribution_contract = \"hardened-service-targets.v1.toml\"", - "distribution_contract = \"different.toml\"", - ), - format!("{HARDENED_MANAGEMENT_CONTRACT}\nunknown = true\n"), + fn exact_myc_and_rhi_instances_resolve_to_static_metadata_only() { + let context = context(RadrootsPathProfile::RepoLocal); + let myc = target(&context, "myc", "primary"); + let rhi = target(&context, "rhi", "secondary"); + + assert_eq!(myc.service_id().as_str(), "myc"); + assert_eq!(myc.instance_id().as_str(), "primary"); + assert_eq!(myc.profile(), RadrootsPathProfile::RepoLocal); + assert_eq!(myc.service_target().service_id(), myc.service_id()); + assert_eq!(myc.management_mode(), "interactive_user_managed"); + assert!(!myc.mode_contract().service_manager_integration); + + assert_eq!(rhi.service_id().as_str(), "rhi"); + assert_eq!(rhi.instance_id().as_str(), "secondary"); + assert_eq!(rhi.service_target().service_id(), rhi.service_id()); + + for rendered in [ + format!("{context:?}"), + format!("{myc:?}"), + format!("{rhi:?}"), ] { - assert!(parse_contract_str(&raw).is_err()); + assert!(!rendered.contains('/')); + assert!(!rendered.contains("state.sqlite")); + assert!(!rendered.contains("instances.toml")); } } #[test] - fn sealed_target_accessors_debug_and_all_metadata_groups_are_qualified() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - let mut target = resolve_runtime_target(&manager, context("myc", "primary", dir.path())) - .expect("target"); - - assert_eq!(target.instance_source(), RuntimeContextSource::BootstrapCli); - assert_eq!( - target.runtime_group(), - ManagedRuntimeGroup::DefinedManagedTarget - ); - assert_eq!(target.management_mode(), Some("interactive_user_managed")); - assert!(target.mode_contract().is_some()); - assert!(target.bootstrap().is_none()); - assert!(target.instance_record().is_none()); - assert!(target.predicted_paths().is_none()); - assert!(!format!("{target:?}").contains(dir.path().to_string_lossy().as_ref())); - assert!(!format!("{manager:?}").contains(dir.path().to_string_lossy().as_ref())); - - for (group, label, posture) in [ - ( - ManagedRuntimeGroup::ActiveManagedTarget, - "active_managed_target", - "active_managed_target", - ), - ( - ManagedRuntimeGroup::DefinedManagedTarget, - "defined_managed_target", - "defined_future_target", - ), - ( - ManagedRuntimeGroup::BootstrapOnly, - "bootstrap_only", - "bootstrap_only_direct_binding", - ), - (ManagedRuntimeGroup::Unknown, "unknown", "unknown_runtime"), - ] { - target.runtime_group = group; - assert_eq!(target.runtime_group().as_str(), label); - assert_eq!(target.runtime_group().posture(), posture); - - let status = inspect_runtime_status(&target, &["start".to_owned()]); - let logs = inspect_runtime_logs(&target); - let config = inspect_runtime_config(&target); - let action = inspect_runtime_action(&target, ManagedRuntimeLifecycleAction::ConfigSet); - assert!(!status.view.detail.is_empty()); - assert!(!logs.view.detail.is_empty()); - assert!(!config.view.detail.is_empty()); - assert!(!action.view.detail.is_empty()); - } + fn unsupported_service_ids_fail_without_fallback_or_effects() { + let context = context(RadrootsPathProfile::ServiceHost); + let error = resolve_runtime_target( + &context, + ServiceId::new("radrootsd").expect("service"), + InstanceId::new("default").expect("instance"), + ) + .expect_err("unsupported target"); + assert_eq!(error, RadrootsRuntimeManagerError::UnsupportedServiceTarget); } #[test] - fn dormant_active_target_paths_cover_registry_tracking_and_health_states() { - let dir = tempdir().expect("tempdir"); - let manager = manager(dir.path()); - let runtime_context = context("myc", "primary", dir.path()); - let mut target = resolve_runtime_target(&manager, runtime_context).expect("target"); - let paths = resolve_instance_paths(manager.shared_paths(), target.context()); - fs::create_dir_all(paths.logs_dir()).expect("logs dir"); - fs::create_dir_all(paths.config_dir()).expect("config dir"); - fs::create_dir_all(paths.run_dir()).expect("run dir"); - fs::write(paths.stdout_log_path(), "stdout").expect("stdout"); - fs::write(paths.stderr_log_path(), "stderr").expect("stderr"); - fs::write(paths.config_path(), "enabled = true").expect("config"); - - target.runtime_group = ManagedRuntimeGroup::ActiveManagedTarget; - target.predicted_paths = Some(paths.clone()); - target.instance_record = Some(ManagedRuntimeInstanceRecord::new( - target.context(), - ManagedRuntimeInstallState::Installed, - )); - - let status = inspect_runtime_status(&target, &["start".to_owned()]); - assert_eq!( - status.availability, - ManagedRuntimeInspectionAvailability::Success - ); - assert_eq!(status.view.state, "installed"); - assert_eq!(status.view.health_state, "stopped"); - assert_eq!(status.view.lifecycle_actions, ["start"]); - - let logs = inspect_runtime_logs(&target); - assert!(logs.view.stdout_log_present); - assert!(logs.view.stderr_log_present); - let config = inspect_runtime_config(&target); - assert_eq!(config.view.state, "ready"); - assert!(config.view.config_present); - - for state in [ - ManagedRuntimeInstallState::NotInstalled, - ManagedRuntimeInstallState::Configured, - ManagedRuntimeInstallState::Failed, + fn production_manager_is_metadata_only_and_contains_no_io_authority() { + let source = include_str!("managed.rs") + .split("\n#[cfg(test)]") + .next() + .expect("production source"); + for forbidden in [ + "std::fs", + "std::process", + "std::path", + "radroots_runtime_paths::RuntimeContext", + "load_registry", + "save_registry", + "register_instance", + "remove_instance", + "ManagedRuntimeInstancePaths", + "ManagedRuntimeArtifactName", + "inspect_runtime_", + "start_process", + "stop_process", + "extract_binary_archive", ] { - target.instance_record = - Some(ManagedRuntimeInstanceRecord::new(target.context(), state)); - let status = inspect_runtime_status(&target, &[]); - assert!(!status.view.install_state.is_empty()); - assert!(!status.view.health_state.is_empty()); - } - - target.instance_record = None; - assert_eq!( - inspect_runtime_status(&target, &[]).view.state, - "not_installed" - ); - assert_eq!(inspect_runtime_config(&target).view.state, "not_installed"); - - for action in [ - ManagedRuntimeLifecycleAction::Install, - ManagedRuntimeLifecycleAction::Uninstall, - ManagedRuntimeLifecycleAction::Start, - ManagedRuntimeLifecycleAction::Stop, - ManagedRuntimeLifecycleAction::Restart, - ManagedRuntimeLifecycleAction::ConfigSet, - ] { - assert_eq!( - inspect_runtime_action(&target, action).view.action, - action.as_str() + assert!( + !source.contains(forbidden), + "metadata-only manager retained `{forbidden}`" ); } - - for state in [ - ManagedRuntimeHealthState::NotInstalled, - ManagedRuntimeHealthState::Stopped, - ManagedRuntimeHealthState::Starting, - ManagedRuntimeHealthState::Running, - ManagedRuntimeHealthState::Degraded, - ManagedRuntimeHealthState::Failed, - ] { - assert!(!super::health_state_label(state).is_empty()); - } - } - - #[test] - fn unsupported_registration_and_all_runtime_group_memberships_are_explicit() { - let dir = tempdir().expect("tempdir"); - let mut manager = manager(dir.path()); - let unsupported = context("other", "default", dir.path()); - assert_eq!( - manager.register_instance(&unsupported, ManagedRuntimeInstallState::Installed), - Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget) - ); - assert_eq!( - manager.remove_instance(&unsupported), - Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget) - ); - - let mut contract = manager.contract().clone(); - contract.managed_runtime_targets.active = vec!["active".to_owned()]; - contract.managed_runtime_targets.defined = vec!["defined".to_owned()]; - contract.managed_runtime_targets.bootstrap_only = vec!["bootstrap".to_owned()]; - assert_eq!( - runtime_group(&contract, "active"), - ManagedRuntimeGroup::ActiveManagedTarget - ); - assert_eq!( - runtime_group(&contract, "defined"), - ManagedRuntimeGroup::DefinedManagedTarget - ); - assert_eq!( - runtime_group(&contract, "bootstrap"), - ManagedRuntimeGroup::BootstrapOnly - ); - assert_eq!( - runtime_group(&contract, "other"), - ManagedRuntimeGroup::Unknown - ); } } diff --git a/crates/runtime_manager/src/model.rs b/crates/runtime_manager/src/model.rs @@ -1,8 +1,9 @@ -use radroots_runtime_distribution::HardenedServiceTargets; -use radroots_runtime_paths::{InstanceId, RuntimeContext, ServiceId}; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use radroots_runtime_distribution::HardenedServiceTargets; +use radroots_runtime_paths::{InstanceId, ServiceId}; +use serde::Deserialize; + #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct RadrootsRuntimeManagementContract { @@ -122,135 +123,3 @@ impl BootstrapRuntimeContract { self.preferred_cli_binding } } - -#[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, -} - -/// Sealed registry state for one typed service instance. -/// -/// ```compile_fail -/// use radroots_runtime_manager::ManagedRuntimeInstanceRecord; -/// -/// let _ = ManagedRuntimeInstanceRecord { -/// service_id: todo!(), -/// instance_id: todo!(), -/// install_state: todo!(), -/// }; -/// ``` -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct ManagedRuntimeInstanceRecord { - service_id: ServiceId, - instance_id: InstanceId, - install_state: ManagedRuntimeInstallState, -} - -impl ManagedRuntimeInstanceRecord { - #[cfg(test)] - #[must_use] - pub(crate) fn new(context: &RuntimeContext, install_state: ManagedRuntimeInstallState) -> Self { - Self { - service_id: context.service().clone(), - instance_id: context.instance().clone(), - install_state, - } - } - - #[must_use] - pub fn service_id(&self) -> &ServiceId { - &self.service_id - } - - #[must_use] - pub fn instance_id(&self) -> &InstanceId { - &self.instance_id - } - - #[must_use] - pub fn install_state(&self) -> ManagedRuntimeInstallState { - self.install_state - } - - #[must_use] - pub fn matches_context(&self, context: &RuntimeContext) -> bool { - self.service_id == *context.service() && self.instance_id == *context.instance() - } -} - -impl core::fmt::Debug for ManagedRuntimeInstanceRecord { - fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - formatter - .debug_struct("ManagedRuntimeInstanceRecord") - .field("service_id", &self.service_id) - .field("instance_id", &self.instance_id) - .field("install_state", &self.install_state) - .finish() - } -} - -pub const RUNTIME_INSTANCE_REGISTRY_SCHEMA: &str = "radroots.service-instance-registry"; -pub const RUNTIME_INSTANCE_REGISTRY_VERSION: u32 = 1; - -/// A sealed, schema-fixed, normalized instance registry. -/// -/// ```compile_fail -/// use radroots_runtime_manager::ManagedRuntimeInstanceRegistry; -/// -/// let _ = ManagedRuntimeInstanceRegistry { -/// schema: "wrong".to_owned(), -/// schema_version: 99, -/// instances: Vec::new(), -/// }; -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct ManagedRuntimeInstanceRegistry { - pub(crate) schema: String, - pub(crate) schema_version: u32, - #[serde(default)] - pub(crate) instances: Vec<ManagedRuntimeInstanceRecord>, -} - -impl ManagedRuntimeInstanceRegistry { - #[must_use] - pub fn schema(&self) -> &str { - &self.schema - } - - #[must_use] - pub fn schema_version(&self) -> u32 { - self.schema_version - } - - #[must_use] - pub fn instances(&self) -> &[ManagedRuntimeInstanceRecord] { - &self.instances - } -} - -impl Default for ManagedRuntimeInstanceRegistry { - fn default() -> Self { - Self { - schema: RUNTIME_INSTANCE_REGISTRY_SCHEMA.to_string(), - schema_version: RUNTIME_INSTANCE_REGISTRY_VERSION, - instances: Vec::new(), - } - } -} diff --git a/crates/runtime_manager/src/paths.rs b/crates/runtime_manager/src/paths.rs @@ -1,373 +0,0 @@ -use core::fmt; -use std::path::{Path, PathBuf}; - -use radroots_runtime_paths::{RuntimeContext, default_service_instance_artifacts}; - -use crate::error::RadrootsRuntimeManagerError; -use crate::model::RadrootsRuntimeManagementContract; - -/// Manager-owned paths derived from the manager's validated service context. -/// -/// External callers cannot forge another root set: -/// -/// ```compile_fail -/// use std::path::PathBuf; -/// use radroots_runtime_manager::ManagedRuntimeSharedPaths; -/// -/// let _ = ManagedRuntimeSharedPaths { -/// instance_registry_path: PathBuf::from("/tmp/escape"), -/// artifact_cache_dir: PathBuf::from("/tmp/escape"), -/// install_root: PathBuf::from("/tmp/escape"), -/// }; -/// ``` -#[derive(Clone, PartialEq, Eq)] -pub struct ManagedRuntimeSharedPaths { - instance_registry_path: PathBuf, - artifact_cache_dir: PathBuf, - install_root: PathBuf, - logs_root: PathBuf, - run_root: PathBuf, -} - -impl ManagedRuntimeSharedPaths { - #[must_use] - pub fn instance_registry_path(&self) -> &Path { - &self.instance_registry_path - } - - #[must_use] - pub fn artifact_cache_dir(&self) -> &Path { - &self.artifact_cache_dir - } - - #[must_use] - pub fn install_root(&self) -> &Path { - &self.install_root - } - - #[must_use] - pub fn logs_root(&self) -> &Path { - &self.logs_root - } - - #[must_use] - pub fn run_root(&self) -> &Path { - &self.run_root - } -} - -impl fmt::Debug for ManagedRuntimeSharedPaths { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("ManagedRuntimeSharedPaths([redacted])") - } -} - -/// Operational paths for one validated service instance. -/// -/// Service-owned directories and fixed artifacts come only from the supplied -/// [`RuntimeContext`]. The manager-owned install directory comes only from its -/// own validated context. Process tracking and captured stdout/stderr remain -/// under manager-owned roots, so lifecycle removal cannot delete canonical -/// service state or secrets. -/// -/// ```compile_fail -/// use std::path::PathBuf; -/// use radroots_runtime_manager::ManagedRuntimeInstancePaths; -/// -/// let _ = ManagedRuntimeInstancePaths { -/// install_dir: PathBuf::from("/tmp/escape"), -/// }; -/// ``` -#[derive(Clone, PartialEq, Eq)] -pub struct ManagedRuntimeInstancePaths { - context: RuntimeContext, - install_dir: PathBuf, - logs_dir: PathBuf, - run_dir: PathBuf, - pid_file_path: PathBuf, - stdout_log_path: PathBuf, - stderr_log_path: PathBuf, -} - -impl ManagedRuntimeInstancePaths { - #[must_use] - pub fn context(&self) -> &RuntimeContext { - &self.context - } - - #[must_use] - pub fn install_dir(&self) -> &Path { - &self.install_dir - } - - #[must_use] - pub fn config_dir(&self) -> &Path { - self.context.paths().config() - } - - #[must_use] - pub fn state_dir(&self) -> &Path { - self.context.paths().state() - } - - #[must_use] - pub fn logs_dir(&self) -> &Path { - &self.logs_dir - } - - #[must_use] - pub fn run_dir(&self) -> &Path { - &self.run_dir - } - - #[must_use] - pub fn secrets_dir(&self) -> &Path { - self.context.paths().secrets() - } - - #[must_use] - pub fn config_path(&self) -> PathBuf { - default_service_instance_artifacts(self.context.paths()) - .config() - .to_path_buf() - } - - #[must_use] - pub fn state_database_path(&self) -> PathBuf { - default_service_instance_artifacts(self.context.paths()) - .state_database() - .to_path_buf() - } - - #[must_use] - pub fn state_lock_path(&self) -> PathBuf { - default_service_instance_artifacts(self.context.paths()) - .state_lock() - .to_path_buf() - } - - #[must_use] - pub fn admin_socket_path(&self) -> PathBuf { - default_service_instance_artifacts(self.context.paths()) - .admin_socket() - .to_path_buf() - } - - #[must_use] - pub fn pid_file_path(&self) -> &Path { - &self.pid_file_path - } - - #[must_use] - pub fn stdout_log_path(&self) -> &Path { - &self.stdout_log_path - } - - #[must_use] - pub fn stderr_log_path(&self) -> &Path { - &self.stderr_log_path - } -} - -impl fmt::Debug for ManagedRuntimeInstancePaths { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("ManagedRuntimeInstancePaths([redacted])") - } -} - -#[must_use] -pub(crate) fn resolve_shared_paths(context: &RuntimeContext) -> ManagedRuntimeSharedPaths { - ManagedRuntimeSharedPaths { - instance_registry_path: context.paths().config().join("instances.toml"), - artifact_cache_dir: context.paths().cache().join("artifacts"), - install_root: context.paths().state().join("installs"), - logs_root: context.paths().logs().join("instances"), - run_root: context.paths().run().join("instances"), - } -} - -#[must_use] -#[cfg(test)] -pub(crate) fn resolve_instance_paths( - shared: &ManagedRuntimeSharedPaths, - context: &RuntimeContext, -) -> ManagedRuntimeInstancePaths { - let suffix = PathBuf::from(context.service().as_str()).join(context.instance().as_str()); - let logs_dir = shared.logs_root.join(&suffix); - let run_dir = shared.run_root.join(&suffix); - - ManagedRuntimeInstancePaths { - context: context.clone(), - install_dir: shared.install_root.join(suffix), - logs_dir: logs_dir.clone(), - run_dir: run_dir.clone(), - pid_file_path: run_dir.join("runtime.pid"), - stdout_log_path: logs_dir.join("stdout.log"), - stderr_log_path: logs_dir.join("stderr.log"), - } -} - -pub fn bootstrap_runtime<'a>( - contract: &'a RadrootsRuntimeManagementContract, - runtime_id: &str, -) -> Result<&'a crate::model::BootstrapRuntimeContract, RadrootsRuntimeManagerError> { - contract - .bootstrap - .get(runtime_id) - .ok_or(RadrootsRuntimeManagerError::UnknownBootstrapRuntime) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use radroots_runtime_paths::{ - InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, - }; - - use super::{resolve_instance_paths, resolve_shared_paths}; - - fn repo_context(service: &str, instance: &str, root: &str) -> RuntimeContext { - RuntimeContext::resolve( - &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), - RuntimeContextBootstrap::new( - RadrootsPathProfile::RepoLocal, - Some(PathBuf::from(root)), - RuntimeContextSource::BootstrapCli, - RuntimeContextSource::BootstrapCli, - ) - .expect("bootstrap"), - ServiceId::new(service).expect("service"), - InstanceId::new(instance).expect("instance"), - ) - .expect("context") - } - - fn service_host_context(service: &str, instance: &str) -> RuntimeContext { - RuntimeContext::resolve( - &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), - RuntimeContextBootstrap::new( - RadrootsPathProfile::ServiceHost, - None, - RuntimeContextSource::SafeDefault, - RuntimeContextSource::BootstrapCli, - ) - .expect("bootstrap"), - ServiceId::new(service).expect("service"), - InstanceId::new(instance).expect("instance"), - ) - .expect("context") - } - - #[test] - fn shared_paths_derive_only_from_the_manager_context() { - let manager = repo_context("runtime-manager", "default", "/repo/.radroots"); - let paths = resolve_shared_paths(&manager); - - assert_eq!( - paths.instance_registry_path(), - PathBuf::from("/repo/.radroots/config/services/runtime-manager/default/instances.toml") - ); - assert_eq!( - paths.artifact_cache_dir(), - PathBuf::from("/repo/.radroots/cache/services/runtime-manager/default/artifacts") - ); - assert_eq!( - paths.install_root(), - PathBuf::from("/repo/.radroots/data/services/runtime-manager/default/installs") - ); - assert_eq!( - format!("{paths:?}"), - "ManagedRuntimeSharedPaths([redacted])" - ); - } - - #[test] - fn instance_paths_use_the_exact_service_context_and_fixed_artifacts() { - let shared = resolve_shared_paths(&repo_context( - "runtime-manager", - "default", - "/repo/.radroots", - )); - let service = repo_context("myc", "north", "/repo/.radroots"); - let paths = resolve_instance_paths(&shared, &service); - - assert_eq!( - paths.install_dir(), - PathBuf::from( - "/repo/.radroots/data/services/runtime-manager/default/installs/myc/north" - ) - ); - assert_eq!( - paths.config_path(), - PathBuf::from("/repo/.radroots/config/services/myc/north/config.toml") - ); - assert_eq!( - paths.state_database_path(), - PathBuf::from("/repo/.radroots/data/services/myc/north/state.sqlite") - ); - assert_eq!( - paths.state_lock_path(), - PathBuf::from("/repo/.radroots/data/services/myc/north/state.lock") - ); - assert_eq!( - paths.admin_socket_path(), - PathBuf::from("/repo/.radroots/run/services/myc/north/admin.sock") - ); - assert_eq!( - paths.stdout_log_path(), - PathBuf::from( - "/repo/.radroots/logs/services/runtime-manager/default/instances/myc/north/stdout.log" - ) - ); - assert_eq!( - format!("{paths:?}"), - "ManagedRuntimeInstancePaths([redacted])" - ); - } - - #[test] - fn multi_instance_paths_cannot_cross_service_contexts() { - let shared = resolve_shared_paths(&repo_context( - "runtime-manager", - "default", - "/repo/.radroots", - )); - let north = - resolve_instance_paths(&shared, &repo_context("rhi", "north", "/repo/.radroots")); - let south = - resolve_instance_paths(&shared, &repo_context("rhi", "south", "/repo/.radroots")); - - assert_ne!(north, south); - assert!(north.state_dir().ends_with("services/rhi/north")); - assert!(south.state_dir().ends_with("services/rhi/south")); - assert!(!north.state_dir().starts_with(south.state_dir())); - assert!(!south.state_dir().starts_with(north.state_dir())); - } - - #[test] - fn linux_service_host_paths_preserve_the_canonical_service_layout() { - let manager = service_host_context("runtime-manager", "default"); - let shared = resolve_shared_paths(&manager); - let service = service_host_context("myc", "primary"); - let paths = resolve_instance_paths(&shared, &service); - - assert_eq!( - shared.instance_registry_path(), - PathBuf::from("/etc/radroots/services/runtime-manager/default/instances.toml") - ); - assert_eq!( - paths.config_path(), - PathBuf::from("/etc/radroots/services/myc/primary/config.toml") - ); - assert_eq!( - paths.state_database_path(), - PathBuf::from("/var/lib/radroots/services/myc/primary/state.sqlite") - ); - assert_eq!( - paths.admin_socket_path(), - PathBuf::from("/run/radroots/services/myc/primary/admin.sock") - ); - } -} diff --git a/crates/runtime_manager/src/registry.rs b/crates/runtime_manager/src/registry.rs @@ -1,392 +0,0 @@ -use std::fs; -use std::path::Path; - -use radroots_runtime_paths::{InstanceId, ServiceId}; - -use crate::error::RadrootsRuntimeManagerError; -use crate::model::{ - ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, RUNTIME_INSTANCE_REGISTRY_SCHEMA, - RUNTIME_INSTANCE_REGISTRY_VERSION, -}; - -pub fn load_registry( - path: impl AsRef<Path>, -) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> { - load_registry_path(path.as_ref()) -} - -fn load_registry_path( - path: &Path, -) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> { - 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 { - kind: source.kind(), - }); - } - }; - - let registry = toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw) - .map_err(|_| RadrootsRuntimeManagerError::ParseRegistry)?; - normalize_registry(registry) -} - -pub fn save_registry( - path: impl AsRef<Path>, - registry: &ManagedRuntimeInstanceRegistry, -) -> Result<(), RadrootsRuntimeManagerError> { - save_registry_path(path.as_ref(), registry) -} - -fn save_registry_path( - path: &Path, - registry: &ManagedRuntimeInstanceRegistry, -) -> Result<(), RadrootsRuntimeManagerError> { - save_registry_path_with(path, registry, toml::to_string_pretty) -} - -fn save_registry_path_with( - path: &Path, - registry: &ManagedRuntimeInstanceRegistry, - serializer: fn(&ManagedRuntimeInstanceRegistry) -> Result<String, toml::ser::Error>, -) -> Result<(), RadrootsRuntimeManagerError> { - ensure_registry_parent(path)?; - - let normalized = normalize_registry(registry.clone())?; - let raw = - serializer(&normalized).map_err(|_| RadrootsRuntimeManagerError::SerializeRegistry)?; - fs::write(path, raw).map_err(|source| RadrootsRuntimeManagerError::WriteRegistry { - kind: source.kind(), - }) -} - -fn normalize_registry( - mut registry: ManagedRuntimeInstanceRegistry, -) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> { - if registry.schema != RUNTIME_INSTANCE_REGISTRY_SCHEMA { - return Err(RadrootsRuntimeManagerError::UnexpectedRegistrySchema); - } - if registry.schema_version != RUNTIME_INSTANCE_REGISTRY_VERSION { - return Err(RadrootsRuntimeManagerError::UnexpectedRegistryVersion); - } - registry.instances.sort_by(|left, right| { - left.service_id() - .cmp(right.service_id()) - .then_with(|| left.instance_id().cmp(right.instance_id())) - }); - if registry.instances.windows(2).any(|pair| { - pair[0].service_id() == pair[1].service_id() - && pair[0].instance_id() == pair[1].instance_id() - }) { - return Err(RadrootsRuntimeManagerError::DuplicateRegistryInstance); - } - Ok(registry) -} - -#[cfg(test)] -pub(crate) fn upsert_instance( - registry: &mut ManagedRuntimeInstanceRegistry, - record: ManagedRuntimeInstanceRecord, -) { - if let Some(existing) = registry.instances.iter_mut().find(|existing| { - existing.service_id() == record.service_id() - && existing.instance_id() == record.instance_id() - }) { - *existing = record; - } else { - registry.instances.push(record); - registry.instances.sort_by(|left, right| { - left.service_id() - .cmp(right.service_id()) - .then_with(|| left.instance_id().cmp(right.instance_id())) - }); - } -} - -pub fn instance<'a>( - registry: &'a ManagedRuntimeInstanceRegistry, - service_id: &ServiceId, - instance_id: &InstanceId, -) -> Option<&'a ManagedRuntimeInstanceRecord> { - registry - .instances - .iter() - .find(|record| record.service_id() == service_id && record.instance_id() == instance_id) -} - -#[cfg(test)] -pub(crate) fn remove_instance( - registry: &mut ManagedRuntimeInstanceRegistry, - service_id: &ServiceId, - instance_id: &InstanceId, -) -> Option<ManagedRuntimeInstanceRecord> { - let index = registry.instances.iter().position(|record| { - record.service_id() == service_id && record.instance_id() == instance_id - })?; - Some(registry.instances.remove(index)) -} - -fn ensure_registry_parent(path: &Path) -> Result<(), RadrootsRuntimeManagerError> { - let Some(parent) = path.parent() else { - return Ok(()); - }; - if parent.as_os_str().is_empty() { - return Ok(()); - } - fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateRegistryParent { - kind: source.kind(), - }) -} - -#[cfg(test)] -mod tests { - use std::fs; - use std::path::{Path, PathBuf}; - - use radroots_runtime_paths::{ - InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, - }; - use serde::ser::Error as _; - use tempfile::tempdir; - - use super::{ - ensure_registry_parent, instance, load_registry, remove_instance, save_registry, - save_registry_path_with, upsert_instance, - }; - use crate::{ - ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, - RadrootsRuntimeManagerError, - }; - - fn runtime_context(service_id: &str, instance_id: &str) -> RuntimeContext { - RuntimeContext::resolve( - &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), - RuntimeContextBootstrap::new( - RadrootsPathProfile::RepoLocal, - Some(PathBuf::from("/repo/.radroots")), - RuntimeContextSource::BootstrapCli, - RuntimeContextSource::BootstrapCli, - ) - .expect("bootstrap"), - ServiceId::new(service_id).expect("service"), - InstanceId::new(instance_id).expect("instance"), - ) - .expect("context") - } - - fn sample_record(service_id: &str, instance_id: &str) -> ManagedRuntimeInstanceRecord { - let context = runtime_context(service_id, instance_id); - ManagedRuntimeInstanceRecord::new(&context, ManagedRuntimeInstallState::Configured) - } - - fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) { - use std::error::Error as _; - - let rendered = err.to_string(); - for part in parts { - assert!( - rendered.contains(part), - "expected `{rendered}` to contain `{part}`" - ); - } - assert!(err.source().is_none()); - } - - #[test] - fn load_registry_returns_default_when_file_is_missing() { - let dir = tempdir().expect("tempdir"); - let registry = load_registry(dir.path().join("missing.toml")).expect("missing registry"); - assert_eq!(registry, ManagedRuntimeInstanceRegistry::default()); - } - - #[test] - fn load_registry_reports_read_errors() { - let dir = tempdir().expect("tempdir"); - let err = load_registry(dir.path()).expect_err("directory should fail"); - assert_error_contains(&err, &["read runtime instance registry", "is a directory"]); - } - - #[test] - fn load_registry_reports_parse_errors() { - let dir = tempdir().expect("tempdir"); - let path = dir.path().join("instances.toml"); - fs::write(&path, "credential = 'secret-value'\nnot = [valid") - .expect("write invalid registry"); - - let err = load_registry(&path).expect_err("invalid registry should fail"); - assert_error_contains(&err, &["parse runtime instance registry"]); - let rendered = format!("{err} {err:?}"); - assert!(!rendered.contains("secret-value")); - assert!(!rendered.contains(path.to_string_lossy().as_ref())); - } - - #[test] - fn save_registry_reports_write_errors() { - let dir = tempdir().expect("tempdir"); - let path = dir.path().join("registry-dir"); - fs::create_dir(&path).expect("create directory target"); - - let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default()) - .expect_err("directory path should fail"); - assert_error_contains(&err, &["write runtime instance registry", "is a directory"]); - } - - #[test] - fn save_registry_reports_parent_creation_errors() { - let dir = tempdir().expect("tempdir"); - let file_parent = dir.path().join("occupied"); - fs::write(&file_parent, "file").expect("occupied parent"); - let path = file_parent.join("instances.toml"); - - let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default()) - .expect_err("file parent should fail"); - assert_error_contains(&err, &["create runtime instance registry parent"]); - } - - #[test] - fn save_registry_reports_serializer_errors() { - let dir = tempdir().expect("tempdir"); - let path = dir.path().join("instances.toml"); - - let err = - save_registry_path_with(&path, &ManagedRuntimeInstanceRegistry::default(), |_| { - Err(toml::ser::Error::custom( - "forced registry serializer failure", - )) - }) - .expect_err("serializer should fail"); - - assert_error_contains(&err, &["serialize runtime instance registry"]); - } - - #[test] - fn ensure_registry_parent_accepts_parentless_relative_paths() { - ensure_registry_parent(Path::new("instances.toml")).expect("relative path parentless"); - ensure_registry_parent(Path::new("/")).expect("root path parentless"); - } - - #[test] - fn upsert_instance_replaces_existing_and_sorts_new_records() { - let mut registry = ManagedRuntimeInstanceRegistry::default(); - upsert_instance(&mut registry, sample_record("radrootsd", "b")); - upsert_instance(&mut registry, sample_record("radrootsd", "a")); - upsert_instance(&mut registry, sample_record("myc", "a")); - - let replacement = ManagedRuntimeInstanceRecord::new( - &runtime_context("radrootsd", "b"), - ManagedRuntimeInstallState::Failed, - ); - upsert_instance(&mut registry, replacement); - - assert_eq!(registry.instances.len(), 3); - assert_eq!(registry.instances[0].service_id().as_str(), "myc"); - assert_eq!(registry.instances[1].instance_id().as_str(), "a"); - assert_eq!(registry.instances[2].service_id().as_str(), "radrootsd"); - assert_eq!(registry.instances[2].instance_id().as_str(), "b"); - assert_eq!( - registry.instances[2].install_state(), - ManagedRuntimeInstallState::Failed - ); - } - - #[test] - fn instance_and_remove_instance_handle_missing_and_present_rows() { - let mut registry = ManagedRuntimeInstanceRegistry::default(); - upsert_instance(&mut registry, sample_record("radrootsd", "local")); - - let myc = ServiceId::new("myc").expect("service"); - let radrootsd = ServiceId::new("radrootsd").expect("service"); - let local = InstanceId::new("local").expect("instance"); - - assert!(instance(&registry, &myc, &local).is_none()); - assert!(remove_instance(&mut registry, &myc, &local).is_none()); - - let removed = remove_instance(&mut registry, &radrootsd, &local).expect("remove"); - assert_eq!(removed.service_id().as_str(), "radrootsd"); - assert!(registry.instances.is_empty()); - } - - #[test] - fn registry_round_trip_is_typed_sorted_and_contains_no_service_paths_or_secrets() { - let dir = tempdir().expect("tempdir"); - let path = dir.path().join("instances.toml"); - let mut registry = ManagedRuntimeInstanceRegistry::default(); - upsert_instance(&mut registry, sample_record("rhi", "secondary")); - upsert_instance(&mut registry, sample_record("myc", "primary")); - - save_registry(&path, &registry).expect("save registry"); - let raw = fs::read_to_string(&path).expect("read registry"); - for forbidden in [ - "binary_path", - "config_path", - "logs_path", - "run_path", - "secrets_path", - "secret_material_ref", - "/repo/", - "/etc/", - ] { - assert!(!raw.contains(forbidden), "registry leaked `{forbidden}`"); - } - - let loaded = load_registry(&path).expect("load registry"); - assert_eq!(loaded, registry); - assert_eq!(loaded.instances[0].service_id().as_str(), "myc"); - assert_eq!(loaded.instances[1].service_id().as_str(), "rhi"); - } - - #[test] - fn registry_rejects_schema_version_unknown_fields_and_duplicate_keys() { - let dir = tempdir().expect("tempdir"); - let path = dir.path().join("instances.toml"); - let registry = ManagedRuntimeInstanceRegistry { - instances: vec![sample_record("myc", "primary")], - ..ManagedRuntimeInstanceRegistry::default() - }; - save_registry(&path, &registry).expect("save registry"); - let raw = fs::read_to_string(&path).expect("read registry"); - - fs::write( - &path, - raw.replace("radroots.service-instance-registry", "wrong"), - ) - .expect("write wrong schema"); - assert!(matches!( - load_registry(&path), - Err(RadrootsRuntimeManagerError::UnexpectedRegistrySchema) - )); - - fs::write( - &path, - raw.replace("schema_version = 1", "schema_version = 2"), - ) - .expect("write wrong version"); - assert!(matches!( - load_registry(&path), - Err(RadrootsRuntimeManagerError::UnexpectedRegistryVersion) - )); - - fs::write(&path, format!("{raw}\nunknown = true\n")).expect("write unknown field"); - assert!(matches!( - load_registry(&path), - Err(RadrootsRuntimeManagerError::ParseRegistry) - )); - - let duplicate = ManagedRuntimeInstanceRegistry { - instances: vec![ - sample_record("myc", "primary"), - sample_record("myc", "primary"), - ], - ..ManagedRuntimeInstanceRegistry::default() - }; - assert!(matches!( - save_registry(&path, &duplicate), - Err(RadrootsRuntimeManagerError::DuplicateRegistryInstance) - )); - } -} diff --git a/crates/runtime_manager/tests/service_target_boundary.rs b/crates/runtime_manager/tests/service_target_boundary.rs @@ -1,8 +1,19 @@ const MANAGEMENT_FIXTURE: &str = include_str!("fixtures/hardened_service_management.v1.toml"); const MANAGER_ROOT_SOURCE: &str = include_str!("../src/lib.rs"); +const MANAGER_SOURCE: &str = include_str!("../src/managed.rs"); +const MODEL_SOURCE: &str = include_str!("../src/model.rs"); +const MANIFEST: &str = include_str!("../Cargo.toml"); +const README: &str = include_str!("../README"); + +fn production_source(source: &str) -> &str { + source + .split("\n#[cfg(test)]") + .next() + .expect("production source") +} #[test] -fn hardened_services_remain_metadata_only_in_management_contract() { +fn hardened_services_remain_exact_metadata_only_targets() { for forbidden in [ "active = [\"myc", "active = [\"rhi", @@ -41,3 +52,56 @@ fn management_contract_is_bounded_before_toml_admission() { "contract size must be checked before parsing" ); } + +#[test] +fn public_package_contains_only_metadata_resolution_authority() { + let production = [ + production_source(MANAGER_ROOT_SOURCE), + production_source(MANAGER_SOURCE), + production_source(MODEL_SOURCE), + ] + .join("\n"); + for forbidden in [ + "std::fs", + "std::process", + "std::path", + "radroots_runtime_paths::RuntimeContext", + "ManagedRuntimeArtifactName", + "ManagedRuntimeInstancePaths", + "ManagedRuntimeSharedPaths", + "ManagedRuntimeInstanceRegistry", + "ManagedRuntimeInstanceRecord", + "load_registry", + "save_registry", + "register_instance", + "remove_instance", + "start_process", + "stop_process", + "process_running", + "install_binary", + "extract_binary_archive", + "remove_instance_artifacts", + "write_instance_config", + "inspect_runtime_", + ] { + assert!( + !production.contains(forbidden), + "production surface retained `{forbidden}`" + ); + } + + for forbidden_dependency in ["flate2", "tar =", "tempfile"] { + assert!( + !MANIFEST.contains(forbidden_dependency), + "manifest retained `{forbidden_dependency}`" + ); + } + + for required in [ + "performs no filesystem, registry, process, archive, artifact", + "runtime paths or raw persistence helpers", + "Steps 219 and 220", + ] { + assert!(README.contains(required), "README omitted `{required}`"); + } +}