cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 4faedf460990e56c744578d615c25ca666a2cc26
parent e5a23a888b61202c04e930f255fb7ca5fd369ade
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Apr 2026 22:48:00 +0000

runtime: manage local radrootsd lifecycle

Diffstat:
MCargo.lock | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 3+++
Msrc/cli.rs | 13++++---------
Msrc/commands/runtime.rs | 13++++++-------
Msrc/render/mod.rs | 38+++++++++++++++++++++++++-------------
Msrc/runtime/management.rs | 969++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/runtime/mod.rs | 5++++-
Msrc/runtime/order.rs | 13++++++++-----
Msrc/runtime/provider.rs | 180++++++++++++++++++++++---------------------------------------------------------
Mtests/listing.rs | 29+++++++++++++++++++----------
Mtests/order.rs | 27++++++++++++++++++---------
Mtests/runtime_management.rs | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mtests/runtime_show.rs | 20+++++++-------------
13 files changed, 1362 insertions(+), 259 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3,6 +3,12 @@ version = 4 [[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -471,6 +477,15 @@ dependencies = [ ] [[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crossbeam-channel" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -614,12 +629,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" [[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1184,6 +1220,18 @@ dependencies = [ ] [[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall", +] + +[[package]] name = "libsqlite3-sys" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1250,6 +1298,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] name = "mio" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1483,6 +1541,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1647,6 +1711,7 @@ dependencies = [ "chacha20poly1305", "chrono", "clap", + "flate2", "getrandom 0.2.17", "radroots_core", "radroots_events", @@ -1658,6 +1723,7 @@ dependencies = [ "radroots_protected_store", "radroots_replica_db", "radroots_replica_sync", + "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_secret_vault", @@ -1666,6 +1732,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "tar", "tempfile", "thiserror 2.0.18", "toml", @@ -1845,11 +1912,22 @@ dependencies = [ ] [[package]] +name = "radroots_runtime_distribution" +version = "0.1.0-alpha.1" +dependencies = [ + "serde", + "thiserror 1.0.69", + "toml", +] + +[[package]] name = "radroots_runtime_manager" version = "0.1.0-alpha.1" dependencies = [ + "flate2", "radroots_runtime_paths", "serde", + "tar", "thiserror 1.0.69", "toml", ] @@ -1960,6 +2038,15 @@ dependencies = [ ] [[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2348,6 +2435,12 @@ dependencies = [ ] [[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2431,6 +2524,17 @@ dependencies = [ ] [[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3413,6 +3517,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] name = "yaml-rust2" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -31,6 +31,7 @@ radroots_nostr_signer = { path = "../lib/crates/nostr_signer" } radroots_protected_store = { path = "../lib/crates/protected_store", features = ["std"] } radroots_replica_db = { path = "../lib/crates/replica_db" } radroots_replica_sync = { path = "../lib/crates/replica_sync" } +radroots_runtime_distribution = { path = "../lib/crates/runtime_distribution" } radroots_runtime_manager = { path = "../lib/crates/runtime_manager" } radroots_runtime_paths = { path = "../lib/crates/runtime_paths" } radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] } @@ -46,4 +47,6 @@ zeroize = "1.8" [dev-dependencies] assert_cmd = "2.0" +flate2 = "1" +tar = "0.4" tempfile = "3.17" diff --git a/src/cli.rs b/src/cli.rs @@ -513,8 +513,8 @@ mod tests { use super::{ AccountCommand, CliArgs, Command, ConfigCommand, JobCommand, JobWatchArgs, ListingCommand, LocalCommand, LocalExportFormatArg, MycCommand, NetCommand, OrderCommand, OrderWatchArgs, - RelayCommand, RpcCommand, RuntimeCommand, RuntimeConfigCommand, SignerCommand, - SyncCommand, SyncWatchArgs, + RelayCommand, RpcCommand, RuntimeCommand, RuntimeConfigCommand, SignerCommand, SyncCommand, + SyncWatchArgs, }; use crate::runtime::config::OutputFormat; use clap::Parser; @@ -922,13 +922,8 @@ mod tests { _ => panic!("unexpected command variant"), } - let runtime_config_show = CliArgs::parse_from([ - "radroots", - "runtime", - "config", - "show", - "radrootsd", - ]); + let runtime_config_show = + CliArgs::parse_from(["radroots", "runtime", "config", "show", "radrootsd"]); match runtime_config_show.command { Command::Runtime(args) => match args.command { RuntimeCommand::Config(runtime_config) => match runtime_config.command { diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs @@ -1,8 +1,9 @@ +use crate::cli::{RuntimeConfigSetArgs, RuntimeTargetArgs}; use crate::domain::runtime::{ - AccountRuntimeView, AccountSecretRuntimeView, CapabilityBindingRuntimeView, - CommandOutput, CommandView, ConfigFilesRuntimeView, ConfigShowView, HyfProviderRuntimeView, - HyfRuntimeView, LegacyPathRuntimeView, LocalRuntimeView, LoggingRuntimeView, - MigrationRuntimeView, MycRuntimeView, OutputRuntimeView, PathsRuntimeView, RelayRuntimeView, + AccountRuntimeView, AccountSecretRuntimeView, CapabilityBindingRuntimeView, CommandOutput, + CommandView, ConfigFilesRuntimeView, ConfigShowView, HyfProviderRuntimeView, HyfRuntimeView, + LegacyPathRuntimeView, LocalRuntimeView, LoggingRuntimeView, MigrationRuntimeView, + MycRuntimeView, OutputRuntimeView, PathsRuntimeView, RelayRuntimeView, ResolvedProviderRuntimeView, RpcRuntimeView, SignerRuntimeView, WorkflowRuntimeView, WritePlaneRuntimeView, }; @@ -17,7 +18,6 @@ use crate::runtime::provider::{ resolve_capability_providers, resolve_hyf_provider, resolve_workflow_provider, resolve_write_plane_provider, }; -use crate::cli::{RuntimeConfigSetArgs, RuntimeTargetArgs}; pub fn show( config: &RuntimeConfig, @@ -300,8 +300,7 @@ pub fn config_show( _logging: &LoggingState, args: &RuntimeTargetArgs, ) -> Result<CommandOutput, RuntimeError> { - let inspection = - inspect_config_show(config, args.runtime.as_str(), args.instance.as_deref())?; + let inspection = inspect_config_show(config, args.runtime.as_str(), args.instance.as_deref())?; Ok(command_output( inspection.availability, CommandView::RuntimeConfigShow(inspection.view), diff --git a/src/render/mod.rs b/src/render/mod.rs @@ -774,7 +774,12 @@ fn render_runtime_action( ) -> Result<(), RuntimeError> { write_context( stdout, - format!("runtime · {} · {}", view.runtime_id, view.action.replace('_', " ")).as_str(), + format!( + "runtime · {} · {}", + view.runtime_id, + view.action.replace('_', " ") + ) + .as_str(), )?; render_pairs( stdout, @@ -800,7 +805,10 @@ fn render_runtime_config_show( stdout: &mut dyn Write, view: &RuntimeManagedConfigView, ) -> Result<(), RuntimeError> { - write_context(stdout, format!("runtime · {} · config", view.runtime_id).as_str())?; + write_context( + stdout, + format!("runtime · {} · config", view.runtime_id).as_str(), + )?; let mut rows = vec![ ("runtime", view.runtime_id.as_str()), ("instance", view.instance_id.as_str()), @@ -828,10 +836,7 @@ fn render_runtime_config_show( )); } if let Some(requires_signer_provider) = view.requires_signer_provider { - rows.push(( - "requires signer provider", - yes_no(requires_signer_provider), - )); + rows.push(("requires signer provider", yes_no(requires_signer_provider))); } render_pairs(stdout, "runtime config", rows.as_slice())?; writeln!(stdout, "detail: {}", view.detail)?; @@ -839,11 +844,11 @@ fn render_runtime_config_show( Ok(()) } -fn render_runtime_logs( - stdout: &mut dyn Write, - view: &RuntimeLogsView, -) -> Result<(), RuntimeError> { - write_context(stdout, format!("runtime · {} · logs", view.runtime_id).as_str())?; +fn render_runtime_logs(stdout: &mut dyn Write, view: &RuntimeLogsView) -> Result<(), RuntimeError> { + write_context( + stdout, + format!("runtime · {} · logs", view.runtime_id).as_str(), + )?; let mut rows = vec![ ("runtime", view.runtime_id.as_str()), ("instance", view.instance_id.as_str()), @@ -869,7 +874,10 @@ fn render_runtime_status( stdout: &mut dyn Write, view: &RuntimeStatusView, ) -> Result<(), RuntimeError> { - write_context(stdout, format!("runtime · {} · status", view.runtime_id).as_str())?; + write_context( + stdout, + format!("runtime · {} · status", view.runtime_id).as_str(), + )?; let mut rows = vec![ ("runtime", view.runtime_id.as_str()), ("instance", view.instance_id.as_str()), @@ -947,7 +955,11 @@ fn render_runtime_status( render_pairs(stdout, "instance record", record_rows.as_slice())?; } if !view.lifecycle_actions.is_empty() { - writeln!(stdout, "lifecycle actions: {}", view.lifecycle_actions.join(", "))?; + writeln!( + stdout, + "lifecycle actions: {}", + view.lifecycle_actions.join(", ") + )?; } writeln!(stdout, "source: {}", view.source)?; Ok(()) diff --git a/src/runtime/management.rs b/src/runtime/management.rs @@ -1,12 +1,19 @@ -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; +use chrono::Utc; +use getrandom::getrandom; +use radroots_runtime_distribution::{RadrootsRuntimeDistributionResolver, RuntimeArtifactRequest}; use radroots_runtime_manager::{ - BootstrapRuntimeContract, ManagedRuntimeHealthState, ManagedRuntimeInstanceRecord, - ManagedRuntimeInstanceRegistry, ManagedRuntimeInstallState, ManagementModeContract, - RadrootsRuntimeManagementContract, parse_contract_str, resolve_instance_paths, - resolve_shared_paths, + BootstrapRuntimeContract, ManagedRuntimeHealthState, ManagedRuntimeInstallState, + ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, ManagementModeContract, + RadrootsRuntimeManagementContract, extract_binary_archive, parse_contract_str, + process_running as managed_process_running, remove_instance, remove_instance_artifacts, + resolve_instance_paths, resolve_shared_paths, save_registry, start_process, stop_process, + upsert_instance, write_instance_metadata, write_managed_file, write_secret_file, }; use radroots_runtime_paths::{RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver}; +use serde::{Deserialize, Serialize}; use crate::domain::runtime::{ RuntimeActionView, RuntimeInstancePathsView, RuntimeInstanceRecordView, RuntimeLogsView, @@ -18,7 +25,74 @@ const MANAGEMENT_CONTRACT_RAW: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../../../foundation/contracts/runtime/management.toml" )); -const DEFERRED_LIFECYCLE_SLICE: &str = "rpv1-rpi.5"; +const DISTRIBUTION_CONTRACT_RAW: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../../foundation/contracts/runtime/distribution.toml" +)); +const RADROOTSD_RUNTIME_ID: &str = "radrootsd"; +const RADROOTSD_BINARY_NAME: &str = "radrootsd"; +const RADROOTSD_ARTIFACT_CHANNEL: &str = "stable"; +const RADROOTSD_DEFAULT_RPC_ADDR: &str = "127.0.0.1:7070"; +const RADROOTSD_DEFAULT_METADATA_NAME: &str = "radrootsd"; +const RADROOTSD_BRIDGE_TOKEN_FILE: &str = "bridge-bearer-token.txt"; +const RADROOTSD_IDENTITY_FILE: &str = "identity.secret.json"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManagedRadrootsdSettingsFile { + metadata: ManagedRadrootsdMetadata, + config: ManagedRadrootsdConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManagedRadrootsdMetadata { + name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManagedRadrootsdConfig { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + relays: Vec<String>, + #[serde(skip_serializing_if = "Option::is_none")] + logs_dir: Option<String>, + rpc: ManagedRadrootsdRpc, + bridge: ManagedRadrootsdBridge, + #[serde(default)] + nip46: ManagedRadrootsdNip46, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManagedRadrootsdRpc { + addr: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManagedRadrootsdBridge { + enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + bearer_token: Option<String>, + delivery_policy: String, + publish_max_attempts: usize, + #[serde(skip_serializing_if = "Option::is_none")] + state_path: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManagedRadrootsdNip46 { + public_jsonrpc_enabled: bool, + session_ttl_secs: u64, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + perms: Vec<String>, +} + +impl Default for ManagedRadrootsdNip46 { + fn default() -> Self { + Self { + public_jsonrpc_enabled: false, + session_ttl_secs: 900, + perms: Vec::new(), + } + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RuntimeCommandAvailability { @@ -159,8 +233,11 @@ pub fn inspect_action( instance_id: Option<&str>, action: RuntimeLifecycleAction, ) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { - let context = load_management_context(config)?; + let mut context = load_management_context(config)?; let target = resolve_runtime_target(&context, runtime_id, instance_id); + if target.runtime_group == RuntimeGroup::ActiveManagedTarget { + return execute_action(config, &mut context, target, action); + } let (availability, view) = action_view(&target, action, None); Ok(RuntimeInspection { availability, view }) } @@ -169,21 +246,26 @@ pub fn inspect_config_set( config: &RuntimeConfig, request: &RuntimeConfigMutationRequest, ) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { - let context = load_management_context(config)?; + let mut context = load_management_context(config)?; let target = resolve_runtime_target( &context, request.runtime_id.as_str(), request.instance_id.as_deref(), ); + if target.runtime_group == RuntimeGroup::ActiveManagedTarget { + return execute_config_set(config, &mut context, target, request); + } let detail = Some(format!( - "requested managed config mutation {}={} for runtime `{}` instance `{}`; generic config mutation lands in {}", - request.key, request.value, target.runtime_id, target.instance_id, DEFERRED_LIFECYCLE_SLICE + "requested managed config mutation {}={} for runtime `{}` instance `{}`; runtime `{}` is not an active managed target in this wave", + request.key, request.value, target.runtime_id, target.instance_id, target.runtime_id )); let (availability, view) = action_view(&target, RuntimeLifecycleAction::ConfigSet, detail); Ok(RuntimeInspection { availability, view }) } -fn load_management_context(config: &RuntimeConfig) -> Result<RuntimeManagementContext, RuntimeError> { +fn load_management_context( + config: &RuntimeConfig, +) -> Result<RuntimeManagementContext, RuntimeError> { let contract = parse_contract_str(MANAGEMENT_CONTRACT_RAW)?; let profile = cli_path_profile(config)?; let overrides = cli_path_overrides(config)?; @@ -235,7 +317,10 @@ fn active_management_mode_for_profile<'a>( .iter() .find(|(_, mode)| { mode.contract_state == "active" - && mode.supported_profiles.iter().any(|entry| entry == &profile_id) + && mode + .supported_profiles + .iter() + .any(|entry| entry == &profile_id) }) .map(|(mode_id, _)| mode_id.as_str()) .ok_or_else(|| { @@ -254,7 +339,11 @@ fn resolve_runtime_target( let bootstrap = context.contract.bootstrap.get(runtime_id).cloned(); let instance_id = requested_instance_id .map(ToOwned::to_owned) - .or_else(|| bootstrap.as_ref().map(|entry| entry.default_instance_id.clone())) + .or_else(|| { + bootstrap + .as_ref() + .map(|entry| entry.default_instance_id.clone()) + }) .unwrap_or_else(|| "default".to_owned()); let instance_source = if requested_instance_id.is_some() { "command_arg".to_owned() @@ -263,7 +352,9 @@ fn resolve_runtime_target( } else { "implicit_default".to_owned() }; - let management_mode = bootstrap.as_ref().map(|entry| entry.management_mode.clone()); + let management_mode = bootstrap + .as_ref() + .map(|entry| entry.management_mode.clone()); let mode_contract = management_mode .as_ref() .and_then(|mode_id| context.contract.mode.get(mode_id).cloned()); @@ -350,7 +441,10 @@ fn status_view(target: &RuntimeTarget, lifecycle_actions: &[String]) -> RuntimeS .mode_contract .as_ref() .map(|mode| mode.uses_absolute_binary_paths), - preferred_cli_binding: target.bootstrap.as_ref().map(|entry| entry.preferred_cli_binding), + preferred_cli_binding: target + .bootstrap + .as_ref() + .map(|entry| entry.preferred_cli_binding), install_state: install_state.to_owned(), health_state: health_state.to_owned(), health_source: health_source.to_owned(), @@ -387,7 +481,7 @@ fn logs_view(target: &RuntimeTarget) -> (RuntimeCommandAvailability, RuntimeLogs }; let detail = match target.runtime_group { RuntimeGroup::ActiveManagedTarget => { - "runtime logs report the managed stdout/stderr locations; lifecycle execution lands in rpv1-rpi.5" + "runtime logs report the managed stdout/stderr locations for the active managed instance" .to_owned() } RuntimeGroup::DefinedManagedTarget => format!( @@ -464,12 +558,12 @@ fn config_show_view( let detail = match target.runtime_group { RuntimeGroup::ActiveManagedTarget => { if config_path.is_some() { - "runtime config show reports the managed config location without mutating bindings or lifecycle state" + "runtime config show reports the managed config location without mutating bindings" .to_owned() } else { format!( - "managed runtime `{}` has no registered instance config yet; config bootstrap lands in {}", - target.runtime_id, DEFERRED_LIFECYCLE_SLICE + "managed runtime `{}` has no registered instance config yet", + target.runtime_id ) } } @@ -504,7 +598,10 @@ fn config_show_view( }, source: "runtime management contract + shared instance registry".to_owned(), detail, - config_format: target.bootstrap.as_ref().map(|entry| entry.config_format.clone()), + config_format: target + .bootstrap + .as_ref() + .map(|entry| entry.config_format.clone()), config_path: config_path.clone(), config_present: config_path .as_deref() @@ -525,6 +622,806 @@ fn config_show_view( ) } +fn execute_action( + config: &RuntimeConfig, + context: &mut RuntimeManagementContext, + target: RuntimeTarget, + action: RuntimeLifecycleAction, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + if target.runtime_id != RADROOTSD_RUNTIME_ID { + let (availability, view) = action_view( + &target, + action, + Some(format!( + "runtime `{}` is not admitted as an active managed implementation in this wave", + target.runtime_id + )), + ); + return Ok(RuntimeInspection { availability, view }); + } + + match action { + RuntimeLifecycleAction::Install => install_managed_radrootsd(config, context, target), + RuntimeLifecycleAction::Start => start_managed_radrootsd(config, context, target), + RuntimeLifecycleAction::Stop => stop_managed_radrootsd(context, target), + RuntimeLifecycleAction::Restart => restart_managed_radrootsd(config, context, target), + RuntimeLifecycleAction::Uninstall => uninstall_managed_radrootsd(context, target), + RuntimeLifecycleAction::ConfigSet => unreachable!("config set is handled separately"), + } +} + +fn execute_config_set( + _config: &RuntimeConfig, + context: &mut RuntimeManagementContext, + target: RuntimeTarget, + request: &RuntimeConfigMutationRequest, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + if target.runtime_id != RADROOTSD_RUNTIME_ID { + let (availability, view) = action_view( + &target, + RuntimeLifecycleAction::ConfigSet, + Some(format!( + "runtime `{}` is not admitted as an active managed implementation in this wave", + target.runtime_id + )), + ); + return Ok(RuntimeInspection { availability, view }); + } + + let Some(predicted_paths) = target.predicted_paths.as_ref() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::ConfigSet, + "active managed runtime is missing predicted instance paths".to_owned(), + )); + }; + let Some(mut record) = target.instance_record.clone() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::ConfigSet, + format!( + "managed runtime `{}` instance `{}` is not installed; run `radroots runtime install {}` first", + target.runtime_id, target.instance_id, target.runtime_id + ), + )); + }; + + let mut settings = load_managed_radrootsd_settings(&record.config_path)?; + let token_path = managed_radrootsd_token_path(predicted_paths); + let identity_path = managed_radrootsd_identity_path(predicted_paths); + apply_managed_radrootsd_config_mutation( + &mut settings, + &mut record, + predicted_paths, + request.key.as_str(), + request.value.as_str(), + token_path.as_path(), + )?; + write_secret_material_state(&settings, &mut record, token_path.as_path())?; + save_managed_radrootsd_settings(record.config_path.as_path(), &settings)?; + write_instance_metadata(predicted_paths, &record)?; + upsert_instance(&mut context.registry, record.clone()); + save_registry( + &context.shared_paths.instance_registry_path, + &context.registry, + )?; + + Ok(RuntimeInspection { + availability: RuntimeCommandAvailability::Success, + view: RuntimeActionView { + action: RuntimeLifecycleAction::ConfigSet.as_str().to_owned(), + runtime_id: target.runtime_id, + instance_id: target.instance_id, + instance_source: target.instance_source, + runtime_group: target.runtime_group.as_str().to_owned(), + state: "configured".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail: format!( + "updated managed {} instance `{}` config key `{}`; config path = {}, identity path = {}", + RADROOTSD_RUNTIME_ID, + record.instance_id, + request.key, + record.config_path.display(), + identity_path.display() + ), + mutates_bindings: false, + next_step: None, + }, + }) +} + +fn install_managed_radrootsd( + _config: &RuntimeConfig, + context: &mut RuntimeManagementContext, + target: RuntimeTarget, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + let Some(predicted_paths) = target.predicted_paths.as_ref() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Install, + "active managed runtime is missing predicted instance paths".to_owned(), + )); + }; + + let artifact = resolve_radrootsd_artifact(&context.shared_paths)?; + let binary_path = extract_binary_archive( + artifact.archive_path.as_path(), + artifact.archive_format.as_str(), + predicted_paths, + artifact.binary_name.as_str(), + )?; + + let rpc_addr = RADROOTSD_DEFAULT_RPC_ADDR.to_owned(); + let health_endpoint = rpc_addr_to_http_url(rpc_addr.as_str())?; + let token_path = managed_radrootsd_token_path(predicted_paths); + let bridge_token = generate_bridge_token()?; + let config_path = predicted_paths.state_dir.join("config.toml"); + let settings = bootstrap_managed_radrootsd_settings( + predicted_paths, + rpc_addr.as_str(), + bridge_token.as_str(), + ); + write_secret_file(token_path.as_path(), bridge_token.as_str())?; + save_managed_radrootsd_settings(config_path.as_path(), &settings)?; + + let record = ManagedRuntimeInstanceRecord { + runtime_id: target.runtime_id.clone(), + instance_id: target.instance_id.clone(), + management_mode: target + .management_mode + .clone() + .unwrap_or_else(|| "interactive_user_managed".to_owned()), + install_state: ManagedRuntimeInstallState::Configured, + binary_path: binary_path.clone(), + config_path: config_path.clone(), + logs_path: predicted_paths.logs_dir.clone(), + run_path: predicted_paths.run_dir.clone(), + installed_version: artifact.version.clone(), + health_endpoint: Some(health_endpoint.clone()), + secret_material_ref: Some(token_path.display().to_string()), + last_started_at: None, + last_stopped_at: None, + notes: Some(format!( + "installed from artifact cache {}", + artifact.archive_path.display() + )), + }; + write_instance_metadata(predicted_paths, &record)?; + upsert_instance(&mut context.registry, record.clone()); + save_registry( + &context.shared_paths.instance_registry_path, + &context.registry, + )?; + + let identity_path = managed_radrootsd_identity_path(predicted_paths); + Ok(RuntimeInspection { + availability: RuntimeCommandAvailability::Success, + view: RuntimeActionView { + action: RuntimeLifecycleAction::Install.as_str().to_owned(), + runtime_id: target.runtime_id, + instance_id: target.instance_id, + instance_source: target.instance_source, + runtime_group: target.runtime_group.as_str().to_owned(), + state: "configured".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail: format!( + "installed managed {RADROOTSD_RUNTIME_ID} instance `{}` from artifact {} to {}; config = {}; identity bootstrap path = {}; health endpoint = {}", + record.instance_id, + artifact.archive_path.display(), + binary_path.display(), + config_path.display(), + identity_path.display(), + health_endpoint + ), + mutates_bindings: false, + next_step: None, + }, + }) +} + +fn start_managed_radrootsd( + config: &RuntimeConfig, + context: &mut RuntimeManagementContext, + target: RuntimeTarget, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + let Some(predicted_paths) = target.predicted_paths.as_ref() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Start, + "active managed runtime is missing predicted instance paths".to_owned(), + )); + }; + let Some(mut record) = target.instance_record.clone() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Start, + format!( + "managed runtime `{}` instance `{}` is not installed; run `radroots runtime install {}` first", + target.runtime_id, target.instance_id, target.runtime_id + ), + )); + }; + + if managed_process_running(predicted_paths)? { + return Ok(RuntimeInspection { + availability: RuntimeCommandAvailability::Success, + view: RuntimeActionView { + action: RuntimeLifecycleAction::Start.as_str().to_owned(), + runtime_id: target.runtime_id, + instance_id: target.instance_id, + instance_source: target.instance_source, + runtime_group: target.runtime_group.as_str().to_owned(), + state: "running".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail: format!( + "managed {} instance `{}` is already running from {}", + RADROOTSD_RUNTIME_ID, + record.instance_id, + record.binary_path.display() + ), + mutates_bindings: false, + next_step: None, + }, + }); + } + + let args = vec![ + "--config".to_owned(), + record.config_path.display().to_string(), + "--identity".to_owned(), + managed_radrootsd_identity_path(predicted_paths) + .display() + .to_string(), + "--allow-generate-identity".to_owned(), + ]; + let envs = managed_radrootsd_start_envs(config); + let pid = start_process(record.binary_path.as_path(), &args, &envs, predicted_paths)?; + record.last_started_at = Some(Utc::now().to_rfc3339()); + write_instance_metadata(predicted_paths, &record)?; + upsert_instance(&mut context.registry, record.clone()); + save_registry( + &context.shared_paths.instance_registry_path, + &context.registry, + )?; + + Ok(RuntimeInspection { + availability: RuntimeCommandAvailability::Success, + view: RuntimeActionView { + action: RuntimeLifecycleAction::Start.as_str().to_owned(), + runtime_id: target.runtime_id, + instance_id: target.instance_id, + instance_source: target.instance_source, + runtime_group: target.runtime_group.as_str().to_owned(), + state: "running".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail: format!( + "started managed {} instance `{}` with pid {} using config {}", + RADROOTSD_RUNTIME_ID, + record.instance_id, + pid, + record.config_path.display() + ), + mutates_bindings: false, + next_step: None, + }, + }) +} + +fn stop_managed_radrootsd( + context: &mut RuntimeManagementContext, + target: RuntimeTarget, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + let Some(predicted_paths) = target.predicted_paths.as_ref() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Stop, + "active managed runtime is missing predicted instance paths".to_owned(), + )); + }; + let Some(mut record) = target.instance_record.clone() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Stop, + format!( + "managed runtime `{}` instance `{}` is not installed", + target.runtime_id, target.instance_id + ), + )); + }; + + let stopped = stop_process(predicted_paths)?; + record.last_stopped_at = Some(Utc::now().to_rfc3339()); + write_instance_metadata(predicted_paths, &record)?; + upsert_instance(&mut context.registry, record.clone()); + save_registry( + &context.shared_paths.instance_registry_path, + &context.registry, + )?; + + Ok(RuntimeInspection { + availability: RuntimeCommandAvailability::Success, + view: RuntimeActionView { + action: RuntimeLifecycleAction::Stop.as_str().to_owned(), + runtime_id: target.runtime_id, + instance_id: target.instance_id, + instance_source: target.instance_source, + runtime_group: target.runtime_group.as_str().to_owned(), + state: "stopped".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail: if stopped { + format!( + "stopped managed {} instance `{}`", + RADROOTSD_RUNTIME_ID, record.instance_id + ) + } else { + format!( + "managed {} instance `{}` was already stopped", + RADROOTSD_RUNTIME_ID, record.instance_id + ) + }, + mutates_bindings: false, + next_step: None, + }, + }) +} + +fn restart_managed_radrootsd( + config: &RuntimeConfig, + context: &mut RuntimeManagementContext, + target: RuntimeTarget, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + let stop_result = stop_managed_radrootsd(context, target.clone())?; + if stop_result.availability != RuntimeCommandAvailability::Success { + return Ok(stop_result); + } + let refreshed_target = resolve_runtime_target( + context, + RADROOTSD_RUNTIME_ID, + Some(target.instance_id.as_str()), + ); + let start_result = start_managed_radrootsd(config, context, refreshed_target)?; + Ok(RuntimeInspection { + availability: start_result.availability, + view: RuntimeActionView { + action: RuntimeLifecycleAction::Restart.as_str().to_owned(), + runtime_id: start_result.view.runtime_id, + instance_id: start_result.view.instance_id, + instance_source: start_result.view.instance_source, + runtime_group: start_result.view.runtime_group, + state: start_result.view.state, + source: start_result.view.source, + detail: format!( + "restarted managed {} instance `{}`", + RADROOTSD_RUNTIME_ID, target.instance_id + ), + mutates_bindings: false, + next_step: None, + }, + }) +} + +fn uninstall_managed_radrootsd( + context: &mut RuntimeManagementContext, + target: RuntimeTarget, +) -> Result<RuntimeInspection<RuntimeActionView>, RuntimeError> { + let Some(predicted_paths) = target.predicted_paths.as_ref() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Uninstall, + "active managed runtime is missing predicted instance paths".to_owned(), + )); + }; + let Some(record) = target.instance_record.clone() else { + return Ok(runtime_action_unconfigured( + &target, + RuntimeLifecycleAction::Uninstall, + format!( + "managed runtime `{}` instance `{}` is not installed", + target.runtime_id, target.instance_id + ), + )); + }; + + let _ = stop_process(predicted_paths); + remove_instance_artifacts(predicted_paths)?; + remove_instance( + &mut context.registry, + record.runtime_id.as_str(), + record.instance_id.as_str(), + ); + save_registry( + &context.shared_paths.instance_registry_path, + &context.registry, + )?; + + Ok(RuntimeInspection { + availability: RuntimeCommandAvailability::Success, + view: RuntimeActionView { + action: RuntimeLifecycleAction::Uninstall.as_str().to_owned(), + runtime_id: target.runtime_id, + instance_id: target.instance_id, + instance_source: target.instance_source, + runtime_group: target.runtime_group.as_str().to_owned(), + state: "uninstalled".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail: format!( + "uninstalled managed {} instance `{}` and removed {}", + RADROOTSD_RUNTIME_ID, + record.instance_id, + predicted_paths + .install_dir + .parent() + .unwrap_or(predicted_paths.install_dir.as_path()) + .display() + ), + mutates_bindings: false, + next_step: None, + }, + }) +} + +fn runtime_action_unconfigured( + target: &RuntimeTarget, + action: RuntimeLifecycleAction, + detail: String, +) -> RuntimeInspection<RuntimeActionView> { + RuntimeInspection { + availability: RuntimeCommandAvailability::Unconfigured, + view: RuntimeActionView { + action: action.as_str().to_owned(), + runtime_id: target.runtime_id.clone(), + instance_id: target.instance_id.clone(), + instance_source: target.instance_source.clone(), + runtime_group: target.runtime_group.as_str().to_owned(), + state: "not_installed".to_owned(), + source: "generic runtime-management command family".to_owned(), + detail, + mutates_bindings: false, + next_step: None, + }, + } +} + +#[derive(Debug, Clone)] +struct ResolvedManagedArtifact { + archive_path: PathBuf, + archive_format: String, + binary_name: String, + version: String, +} + +fn resolve_radrootsd_artifact( + shared_paths: &radroots_runtime_manager::ManagedRuntimeSharedPaths, +) -> Result<ResolvedManagedArtifact, RuntimeError> { + let resolver = RadrootsRuntimeDistributionResolver::parse_str(DISTRIBUTION_CONTRACT_RAW)?; + let request = RuntimeArtifactRequest { + runtime_id: RADROOTSD_RUNTIME_ID, + os: current_distribution_os(), + arch: current_distribution_arch(), + version: "0.0.0", + channel: Some(RADROOTSD_ARTIFACT_CHANNEL), + }; + let artifact = resolver.resolve_artifact(&request)?; + let search_root = shared_paths.artifact_cache_dir.join(RADROOTSD_RUNTIME_ID); + let matches = find_cached_artifacts( + search_root.as_path(), + RADROOTSD_RUNTIME_ID, + artifact.target_id.as_str(), + artifact.archive_extension.as_str(), + )?; + match matches.as_slice() { + [] => Err(RuntimeError::Config(format!( + "no cached {RADROOTSD_RUNTIME_ID} artifact found under {} for target {}{}", + search_root.display(), + artifact.target_id, + artifact.archive_extension + ))), + [found] => Ok(found.clone()), + _ => Err(RuntimeError::Config(format!( + "multiple cached {RADROOTSD_RUNTIME_ID} artifacts found under {}; keep exactly one matching target {}{}", + search_root.display(), + artifact.target_id, + artifact.archive_extension + ))), + } +} + +fn find_cached_artifacts( + root: &Path, + runtime_id: &str, + target_id: &str, + extension: &str, +) -> Result<Vec<ResolvedManagedArtifact>, RuntimeError> { + let mut matches = Vec::new(); + if !root.exists() { + return Ok(matches); + } + collect_cached_artifacts(root, runtime_id, target_id, extension, &mut matches)?; + Ok(matches) +} + +fn collect_cached_artifacts( + root: &Path, + runtime_id: &str, + target_id: &str, + extension: &str, + matches: &mut Vec<ResolvedManagedArtifact>, +) -> Result<(), RuntimeError> { + for entry in fs::read_dir(root)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + collect_cached_artifacts(path.as_path(), runtime_id, target_id, extension, matches)?; + continue; + } + let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else { + continue; + }; + let prefix = format!("{runtime_id}-"); + let suffix = format!("-{target_id}{extension}"); + if !file_name.starts_with(prefix.as_str()) || !file_name.ends_with(suffix.as_str()) { + continue; + } + let version = file_name + .strip_prefix(prefix.as_str()) + .and_then(|value| value.strip_suffix(suffix.as_str())) + .ok_or_else(|| { + RuntimeError::Config(format!( + "invalid cached artifact name `{file_name}` under {}", + root.display() + )) + })?; + matches.push(ResolvedManagedArtifact { + archive_path: path.clone(), + archive_format: archive_format_from_extension(extension).to_owned(), + binary_name: RADROOTSD_BINARY_NAME.to_owned(), + version: version.to_owned(), + }); + } + Ok(()) +} + +fn archive_format_from_extension(extension: &str) -> &str { + match extension { + ".tar.gz" => "tar.gz", + other => other.trim_start_matches('.'), + } +} + +fn bootstrap_managed_radrootsd_settings( + predicted_paths: &radroots_runtime_manager::ManagedRuntimeInstancePaths, + rpc_addr: &str, + bridge_token: &str, +) -> ManagedRadrootsdSettingsFile { + ManagedRadrootsdSettingsFile { + metadata: ManagedRadrootsdMetadata { + name: RADROOTSD_DEFAULT_METADATA_NAME.to_owned(), + }, + config: ManagedRadrootsdConfig { + relays: Vec::new(), + logs_dir: Some(predicted_paths.logs_dir.display().to_string()), + rpc: ManagedRadrootsdRpc { + addr: rpc_addr.to_owned(), + }, + bridge: ManagedRadrootsdBridge { + enabled: true, + bearer_token: Some(bridge_token.to_owned()), + delivery_policy: "any".to_owned(), + publish_max_attempts: 2, + state_path: Some( + predicted_paths + .state_dir + .join("bridge/bridge-jobs.json") + .display() + .to_string(), + ), + }, + nip46: ManagedRadrootsdNip46::default(), + }, + } +} + +fn load_managed_radrootsd_settings( + path: &Path, +) -> Result<ManagedRadrootsdSettingsFile, RuntimeError> { + let raw = fs::read_to_string(path)?; + toml::from_str(raw.as_str()).map_err(|err| { + RuntimeError::Config(format!( + "parse managed {RADROOTSD_RUNTIME_ID} config {}: {err}", + path.display() + )) + }) +} + +fn save_managed_radrootsd_settings( + path: &Path, + settings: &ManagedRadrootsdSettingsFile, +) -> Result<(), RuntimeError> { + let raw = toml::to_string_pretty(settings).map_err(|err| { + RuntimeError::Config(format!( + "serialize managed {RADROOTSD_RUNTIME_ID} config {}: {err}", + path.display() + )) + })?; + write_managed_file(path, raw.as_str())?; + Ok(()) +} + +fn apply_managed_radrootsd_config_mutation( + settings: &mut ManagedRadrootsdSettingsFile, + record: &mut ManagedRuntimeInstanceRecord, + predicted_paths: &radroots_runtime_manager::ManagedRuntimeInstancePaths, + key: &str, + value: &str, + token_path: &Path, +) -> Result<(), RuntimeError> { + match key { + "metadata.name" => { + settings.metadata.name = non_empty_value(key, value)?; + } + "config.logs_dir" => { + settings.config.logs_dir = Some(non_empty_value(key, value)?); + } + "config.rpc.addr" => { + let rpc_addr = non_empty_value(key, value)?; + settings.config.rpc.addr = rpc_addr.clone(); + record.health_endpoint = Some(rpc_addr_to_http_url(rpc_addr.as_str())?); + } + "config.bridge.enabled" => { + let enabled = parse_bool(value, key)?; + settings.config.bridge.enabled = enabled; + if !enabled { + settings.config.bridge.bearer_token = None; + if token_path.exists() { + fs::remove_file(token_path)?; + } + record.secret_material_ref = None; + } + } + "config.bridge.bearer_token" => { + let token = non_empty_value(key, value)?; + settings.config.bridge.enabled = true; + settings.config.bridge.bearer_token = Some(token.clone()); + write_secret_file(token_path, token.as_str())?; + record.secret_material_ref = Some(token_path.display().to_string()); + } + "config.bridge.state_path" => { + settings.config.bridge.state_path = Some(non_empty_value(key, value)?); + } + other => { + return Err(RuntimeError::Config(format!( + "unsupported managed {RADROOTSD_RUNTIME_ID} config key `{other}`; supported keys: metadata.name, config.logs_dir, config.rpc.addr, config.bridge.enabled, config.bridge.bearer_token, config.bridge.state_path" + ))); + } + } + + if settings.config.logs_dir.is_none() { + settings.config.logs_dir = Some(predicted_paths.logs_dir.display().to_string()); + } + if settings.config.bridge.state_path.is_none() { + settings.config.bridge.state_path = Some( + predicted_paths + .state_dir + .join("bridge/bridge-jobs.json") + .display() + .to_string(), + ); + } + Ok(()) +} + +fn write_secret_material_state( + settings: &ManagedRadrootsdSettingsFile, + record: &mut ManagedRuntimeInstanceRecord, + token_path: &Path, +) -> Result<(), RuntimeError> { + if settings.config.bridge.enabled { + let token = settings + .config + .bridge + .bearer_token + .as_deref() + .ok_or_else(|| { + RuntimeError::Config(format!( + "managed {RADROOTSD_RUNTIME_ID} bridge is enabled but bearer_token is missing" + )) + })?; + write_secret_file(token_path, token)?; + record.secret_material_ref = Some(token_path.display().to_string()); + } else { + record.secret_material_ref = None; + } + Ok(()) +} + +fn managed_radrootsd_start_envs(config: &RuntimeConfig) -> Vec<(String, String)> { + let mut envs = Vec::new(); + envs.push(( + "RADROOTSD_PATHS_PROFILE".to_owned(), + config.paths.profile.clone(), + )); + if config.paths.profile == "repo_local" { + if let Some(root) = &config.paths.repo_local_root { + envs.push(( + "RADROOTSD_PATHS_REPO_LOCAL_ROOT".to_owned(), + root.display().to_string(), + )); + } + } + envs +} + +fn managed_radrootsd_token_path( + predicted_paths: &radroots_runtime_manager::ManagedRuntimeInstancePaths, +) -> PathBuf { + predicted_paths + .secrets_dir + .join(RADROOTSD_BRIDGE_TOKEN_FILE) +} + +fn managed_radrootsd_identity_path( + predicted_paths: &radroots_runtime_manager::ManagedRuntimeInstancePaths, +) -> PathBuf { + predicted_paths.secrets_dir.join(RADROOTSD_IDENTITY_FILE) +} + +fn current_distribution_os() -> &'static str { + match std::env::consts::OS { + "macos" => "macos", + "linux" => "linux", + "windows" => "windows", + other => other, + } +} + +fn current_distribution_arch() -> &'static str { + match std::env::consts::ARCH { + "x86_64" => "amd64", + "aarch64" => "arm64", + other => other, + } +} + +fn non_empty_value(key: &str, value: &str) -> Result<String, RuntimeError> { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(RuntimeError::Config(format!( + "managed config key `{key}` must not be empty" + ))); + } + Ok(trimmed.to_owned()) +} + +fn parse_bool(value: &str, key: &str) -> Result<bool, RuntimeError> { + match value.trim() { + "true" => Ok(true), + "false" => Ok(false), + other => Err(RuntimeError::Config(format!( + "managed config key `{key}` must be `true` or `false`, got `{other}`" + ))), + } +} + +fn rpc_addr_to_http_url(value: &str) -> Result<String, RuntimeError> { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(RuntimeError::Config( + "managed rpc addr must not be empty".to_owned(), + )); + } + if trimmed.contains("://") { + return Ok(trimmed.to_owned()); + } + Ok(format!("http://{trimmed}")) +} + +fn generate_bridge_token() -> Result<String, RuntimeError> { + let mut bytes = [0_u8; 24]; + getrandom(&mut bytes) + .map_err(|err| RuntimeError::Config(format!("generate bridge token: {err}")))?; + Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect()) +} + fn action_view( target: &RuntimeTarget, action: RuntimeLifecycleAction, @@ -536,13 +1433,12 @@ fn action_view( "deferred", detail_override.unwrap_or_else(|| { format!( - "runtime {} `{}` is reserved for {} so lifecycle execution can land after the generic command family is stable", + "runtime {} `{}` is not supported for this managed target", action.as_str().replace('_', " "), - target.runtime_id, - DEFERRED_LIFECYCLE_SLICE + target.runtime_id ) }), - Some(DEFERRED_LIFECYCLE_SLICE.to_owned()), + None, ), RuntimeGroup::DefinedManagedTarget => ( RuntimeCommandAvailability::Unsupported, @@ -609,18 +1505,16 @@ fn status_detail(target: &RuntimeTarget) -> String { match target.runtime_group { RuntimeGroup::ActiveManagedTarget => match &target.instance_record { Some(record) => format!( - "managed runtime `{}` instance `{}` is registered with config at {}; generic lifecycle execution lands in {}", + "managed runtime `{}` instance `{}` is registered with config at {}", target.runtime_id, target.instance_id, - record.config_path.display(), - DEFERRED_LIFECYCLE_SLICE + record.config_path.display() ), None => format!( - "managed runtime `{}` has no registered instance `{}` in {}; lifecycle bootstrap lands in {}", + "managed runtime `{}` has no registered instance `{}` in {}", target.runtime_id, target.instance_id, - target.registry_path.display(), - DEFERRED_LIFECYCLE_SLICE + target.registry_path.display() ), }, RuntimeGroup::DefinedManagedTarget => format!( @@ -656,13 +1550,14 @@ fn infer_health_state(target: &RuntimeTarget) -> (&'static str, &'static str) { ); } - let pid_path = target - .predicted_paths - .as_ref() - .map(|paths| paths.pid_file_path.clone()) - .unwrap_or_else(|| record.run_path.join("runtime.pid")); - - if pid_path.exists() { + if let Some(paths) = target.predicted_paths.as_ref() { + if managed_process_running(paths).unwrap_or(false) { + return ( + health_state_label(ManagedRuntimeHealthState::Running), + "process_probe", + ); + } + } else if record.run_path.join("runtime.pid").exists() { return ( health_state_label(ManagedRuntimeHealthState::Running), "pid_file_presence", diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs @@ -36,6 +36,8 @@ pub enum RuntimeError { Io(#[from] std::io::Error), #[error("runtime manager error: {0}")] RuntimeManager(#[from] radroots_runtime_manager::RadrootsRuntimeManagerError), + #[error("runtime distribution error: {0}")] + RuntimeDistribution(#[from] radroots_runtime_distribution::RadrootsRuntimeDistributionError), } impl RuntimeError { @@ -48,7 +50,8 @@ impl RuntimeError { | Self::ReplicaSync(_) | Self::Json(_) | Self::Io(_) - | Self::RuntimeManager(_) => ExitCode::from(1), + | Self::RuntimeManager(_) + | Self::RuntimeDistribution(_) => ExitCode::from(1), } } } diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -419,11 +419,14 @@ pub fn submit( }); } - let signer_authority = - match resolve_actor_write_authority(config, "buyer", loaded.document.order.buyer_pubkey.as_str()) { - Ok(authority) => authority, - Err(error) => return Ok(order_binding_error_view(config, &loaded, args, error)), - }; + let signer_authority = match resolve_actor_write_authority( + config, + "buyer", + loaded.document.order.buyer_pubkey.as_str(), + ) { + Ok(authority) => authority, + Err(error) => return Ok(order_binding_error_view(config, &loaded, args, error)), + }; let signer_session_id = match daemon::resolve_signer_session_id( config, diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs @@ -1,8 +1,6 @@ -use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use radroots_runtime_manager::{ManagedRuntimeInstallState, load_registry}; -use serde::Deserialize; +use radroots_runtime_manager::{ManagedRuntimeInstallState, load_registry, read_secret_file}; use url::Url; use crate::runtime::config::{ @@ -392,119 +390,57 @@ fn resolve_managed_write_plane_instance( registry_path.display() )); } - load_managed_radrootsd_target(record.config_path.as_path(), instance_id) -} - -fn runtime_manager_registry_path(config: &RuntimeConfig) -> Result<PathBuf, String> { - let Some(app_dir) = config.paths.app_config_path.parent() else { - return Err("resolve cli app config directory for runtime-manager lookup".to_owned()); - }; - let Some(apps_dir) = app_dir.parent() else { - return Err("resolve cli apps config root for runtime-manager lookup".to_owned()); - }; - let Some(config_root) = apps_dir.parent() else { - return Err("resolve cli config root for runtime-manager lookup".to_owned()); - }; - Ok(config_root.join("shared/runtime-manager/instances.toml")) -} - -#[derive(Debug, Deserialize)] -struct ManagedRadrootsdSettingsFile { - config: ManagedRadrootsdConfigFile, -} - -#[derive(Debug, Deserialize, Default)] -struct ManagedRadrootsdConfigFile { - #[serde(default)] - rpc: ManagedRadrootsdRpcConfig, - #[serde(default)] - rpc_addr: Option<String>, - #[serde(default)] - bridge: ManagedRadrootsdBridgeConfig, -} - -#[derive(Debug, Deserialize)] -struct ManagedRadrootsdRpcConfig { - #[serde(default = "default_managed_radrootsd_rpc_addr")] - addr: String, -} - -impl Default for ManagedRadrootsdRpcConfig { - fn default() -> Self { - Self { - addr: default_managed_radrootsd_rpc_addr(), - } - } -} - -#[derive(Debug, Deserialize, Default)] -struct ManagedRadrootsdBridgeConfig { - #[serde(default)] - enabled: bool, - #[serde(default)] - bearer_token: Option<String>, -} - -fn default_managed_radrootsd_rpc_addr() -> String { - "127.0.0.1:7070".to_owned() -} - -fn load_managed_radrootsd_target( - config_path: &Path, - instance_id: &str, -) -> Result<ResolvedWritePlaneTarget, String> { - let raw = fs::read_to_string(config_path).map_err(|err| { - format!( - "read managed radrootsd config for instance `{instance_id}` at {}: {err}", - config_path.display() - ) - })?; - let settings: ManagedRadrootsdSettingsFile = toml::from_str(raw.as_str()).map_err(|err| { - format!( - "parse managed radrootsd config for instance `{instance_id}` at {}: {err}", - config_path.display() - ) - })?; - if !settings.config.bridge.enabled { - return Err(format!( - "managed radrootsd instance `{instance_id}` has bridge ingress disabled in {}", - config_path.display() - )); - } - let Some(bridge_bearer_token) = settings - .config - .bridge - .bearer_token + let Some(health_endpoint) = record + .health_endpoint .as_deref() .map(str::trim) - .filter(|token| !token.is_empty()) - .map(ToOwned::to_owned) + .filter(|value| !value.is_empty()) else { return Err(format!( - "managed radrootsd instance `{instance_id}` is missing bridge bearer_token in {}", - config_path.display() + "managed radrootsd instance `{instance_id}` is missing health_endpoint in {}", + registry_path.display() )); }; - let rpc_addr = settings - .config - .rpc_addr + let url = validate_write_plane_url(health_endpoint)?; + let Some(secret_material_ref) = record + .secret_material_ref .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) - .unwrap_or(settings.config.rpc.addr.as_str()); - let url = rpc_addr_to_url(rpc_addr)?; + else { + return Err(format!( + "managed radrootsd instance `{instance_id}` is missing secret_material_ref in {}", + registry_path.display() + )); + }; + let bridge_bearer_token = read_secret_file(secret_material_ref).map_err(|err| { + format!( + "read managed radrootsd secret material for instance `{instance_id}` at {secret_material_ref}: {err}" + ) + })?; + let bridge_bearer_token = bridge_bearer_token.trim().to_owned(); + if bridge_bearer_token.is_empty() { + return Err(format!( + "managed radrootsd instance `{instance_id}` has empty secret material at {secret_material_ref}" + )); + } Ok(ResolvedWritePlaneTarget { url, bridge_bearer_token, }) } -fn rpc_addr_to_url(value: &str) -> Result<String, String> { - let trimmed = value.trim(); - if trimmed.contains("://") { - return validate_write_plane_url(trimmed); - } - validate_write_plane_url(format!("http://{trimmed}").as_str()) +fn runtime_manager_registry_path(config: &RuntimeConfig) -> Result<PathBuf, String> { + let Some(app_dir) = config.paths.app_config_path.parent() else { + return Err("resolve cli app config directory for runtime-manager lookup".to_owned()); + }; + let Some(apps_dir) = app_dir.parent() else { + return Err("resolve cli apps config root for runtime-manager lookup".to_owned()); + }; + let Some(config_root) = apps_dir.parent() else { + return Err("resolve cli config root for runtime-manager lookup".to_owned()); + }; + Ok(config_root.join("shared/runtime-manager/instances.toml")) } fn validate_write_plane_url(value: &str) -> Result<String, String> { @@ -586,7 +522,7 @@ fn hyf_executable( #[cfg(test)] mod tests { use std::fs; - use std::path::{Path, PathBuf}; + use std::path::PathBuf; use radroots_runtime_paths::RadrootsMigrationReport; use radroots_secret_vault::RadrootsSecretBackend; @@ -747,11 +683,13 @@ mod tests { fs::create_dir_all(registry_path.parent().expect("registry parent")) .expect("create registry parent"); let managed_config_path = dir.path().join("radrootsd-config.toml"); - write_managed_radrootsd_config( - managed_config_path.as_path(), - "127.0.0.1:7444", - "managed-bridge-token", - ); + let bridge_token_path = dir.path().join("bridge-token.txt"); + fs::write( + &managed_config_path, + "[metadata]\nname = \"managed-radrootsd\"\n", + ) + .expect("write managed config"); + fs::write(&bridge_token_path, "managed-bridge-token").expect("write token"); fs::write( &registry_path, format!( @@ -768,8 +706,11 @@ config_path = "{}" logs_path = "/tmp/logs" run_path = "/tmp/run" installed_version = "0.1.0" +health_endpoint = "http://127.0.0.1:7444" +secret_material_ref = "{}" "#, - managed_config_path.display() + managed_config_path.display(), + bridge_token_path.display() ), ) .expect("write registry"); @@ -829,25 +770,4 @@ installed_version = "0.1.0" assert_eq!(providers[1].capability_id, "workflow.trade"); assert_eq!(providers[2].capability_id, "inference.hyf_stdio"); } - - fn write_managed_radrootsd_config(path: &Path, rpc_addr: &str, bearer_token: &str) { - fs::write( - path, - format!( - r#"[metadata] -name = "managed-radrootsd" - -[config] - -[config.rpc] -addr = "{rpc_addr}" - -[config.bridge] -enabled = true -bearer_token = "{bearer_token}" -"# - ), - ) - .expect("write managed radrootsd config"); - } } diff --git a/tests/listing.rs b/tests/listing.rs @@ -707,7 +707,7 @@ fn listing_publish_uses_myc_binding_before_resolving_daemon_signer_session() { &public_identity, "conn_listing_binding_01", ) - .to_string(), + .to_string(), ) .as_str(), ); @@ -1015,14 +1015,16 @@ fn listing_publish_rejects_daemon_session_with_mismatched_myc_authority() { let server = MockRpcServer::start(move |body, _auth_header| { recorded.lock().expect("recorded").push(body.clone()); match body["method"].as_str().unwrap_or_default() { - "nip46.session.list" => MockRpcResponse::success(json!([sample_session_with_authority( - "sess_mismatch_01", - seller_pubkey.as_str(), - &["sign_event:30402"], - true, - Some("acct_wrong"), - Some("conn_listing_binding_03"), - )])), + "nip46.session.list" => { + MockRpcResponse::success(json!([sample_session_with_authority( + "sess_mismatch_01", + seller_pubkey.as_str(), + &["sign_event:30402"], + true, + Some("acct_wrong"), + Some("conn_listing_binding_03"), + )])) + } _ => MockRpcResponse::rpc_error(-32601, "unexpected rpc method"), } }); @@ -1605,7 +1607,14 @@ fn sample_session( permissions: &[&str], authorized: bool, ) -> Value { - sample_session_with_authority(session_id, signer_pubkey, permissions, authorized, None, None) + sample_session_with_authority( + session_id, + signer_pubkey, + permissions, + authorized, + None, + None, + ) } fn sample_session_with_authority( diff --git a/tests/order.rs b/tests/order.rs @@ -803,14 +803,16 @@ fn order_submit_uses_myc_binding_before_resolving_daemon_signer_session() { auth_header, }); match body["method"].as_str().unwrap_or_default() { - "nip46.session.list" => MockRpcResponse::success(json!([sample_session_with_authority( - "sess_order_02", - buyer_pubkey.as_str(), - &["sign_event"], - true, - Some(session_account_id.as_str()), - Some("conn_order_binding_01") - )])), + "nip46.session.list" => { + MockRpcResponse::success(json!([sample_session_with_authority( + "sess_order_02", + buyer_pubkey.as_str(), + &["sign_event"], + true, + Some(session_account_id.as_str()), + Some("conn_order_binding_01") + )])) + } "bridge.order.request" => MockRpcResponse::success(serde_json::json!({ "deduplicated": false, "job": sample_bridge_job("job_order_02", "accepted", false, "sess_order_02"), @@ -1178,7 +1180,14 @@ fn sample_session( permissions: &[&str], authorized: bool, ) -> Value { - sample_session_with_authority(session_id, signer_pubkey, permissions, authorized, None, None) + sample_session_with_authority( + session_id, + signer_pubkey, + permissions, + authorized, + None, + None, + ) } fn sample_session_with_authority( diff --git a/tests/runtime_management.rs b/tests/runtime_management.rs @@ -1,20 +1,26 @@ use std::fs; -use std::path::Path; +use std::fs::File; +use std::path::{Path, PathBuf}; use std::process::Command; +use std::thread; +use std::time::Duration; use assert_cmd::prelude::*; +use flate2::Compression; +use flate2::write::GzEncoder; use serde_json::Value; +use tar::Builder; use tempfile::tempdir; -fn appdata_root(workdir: &Path) -> std::path::PathBuf { +fn appdata_root(workdir: &Path) -> PathBuf { workdir.join("roaming").join("Radroots") } -fn localappdata_root(workdir: &Path) -> std::path::PathBuf { +fn localappdata_root(workdir: &Path) -> PathBuf { workdir.join("local").join("Radroots") } -fn interactive_root(workdir: &Path) -> std::path::PathBuf { +fn interactive_root(workdir: &Path) -> PathBuf { if cfg!(windows) { localappdata_root(workdir) } else { @@ -22,7 +28,7 @@ fn interactive_root(workdir: &Path) -> std::path::PathBuf { } } -fn config_root(workdir: &Path) -> std::path::PathBuf { +fn config_root(workdir: &Path) -> PathBuf { if cfg!(windows) { appdata_root(workdir).join("config") } else { @@ -30,10 +36,25 @@ fn config_root(workdir: &Path) -> std::path::PathBuf { } } -fn runtime_manager_registry_path(workdir: &Path) -> std::path::PathBuf { +fn cache_root(workdir: &Path) -> PathBuf { + if cfg!(windows) { + localappdata_root(workdir).join("cache") + } else { + interactive_root(workdir).join("cache") + } +} + +fn runtime_manager_registry_path(workdir: &Path) -> PathBuf { config_root(workdir).join("shared/runtime-manager/instances.toml") } +fn runtime_manager_artifact_cache_dir(workdir: &Path) -> PathBuf { + cache_root(workdir) + .join("shared/runtime-manager/artifacts") + .join("radrootsd") + .join("stable") +} + fn runtime_command_in(workdir: &Path) -> Command { let mut command = Command::cargo_bin("radroots").expect("binary"); command.current_dir(workdir); @@ -68,6 +89,46 @@ fn runtime_command_in(workdir: &Path) -> Command { command } +#[cfg(not(windows))] +fn current_server_target_id() -> &'static str { + if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { + "aarch64-apple-darwin" + } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { + "x86_64-apple-darwin" + } else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") { + "aarch64-unknown-linux-gnu" + } else if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") { + "x86_64-unknown-linux-gnu" + } else { + panic!("unsupported host target for runtime-management tests") + } +} + +#[cfg(not(windows))] +fn write_cached_radrootsd_artifact(workdir: &Path) -> PathBuf { + let artifact_dir = runtime_manager_artifact_cache_dir(workdir); + fs::create_dir_all(&artifact_dir).expect("artifact dir"); + let file_name = format!( + "radrootsd-0.1.0-alpha.1-{}.tar.gz", + current_server_target_id() + ); + let archive_path = artifact_dir.join(file_name); + let script = artifact_dir.join("radrootsd"); + fs::write( + &script, + "#!/bin/sh\nprintf 'managed radrootsd started\\n' >> \"${TMPDIR:-/tmp}/radrootsd-managed.log\"\nsleep 30\n", + ) + .expect("write script"); + let file = File::create(&archive_path).expect("archive file"); + let encoder = GzEncoder::new(file, Compression::default()); + let mut builder = Builder::new(encoder); + builder + .append_path_with_name(&script, "radrootsd/bin/radrootsd") + .expect("append script"); + builder.finish().expect("finish archive"); + archive_path +} + #[test] fn runtime_status_reports_active_managed_target_truth() { let dir = tempdir().expect("tempdir"); @@ -108,22 +169,99 @@ fn runtime_status_reports_defined_future_target_truth() { assert_eq!(json["lifecycle_actions"], Value::Array(vec![])); } +#[cfg(not(windows))] #[test] -fn runtime_install_is_exposed_but_truthfully_deferred() { +fn runtime_manages_radrootsd_lifecycle_end_to_end() { let dir = tempdir().expect("tempdir"); - let output = runtime_command_in(dir.path()) + let artifact_path = write_cached_radrootsd_artifact(dir.path()); + + let install = runtime_command_in(dir.path()) .args(["--json", "runtime", "install", "radrootsd"]) .output() .expect("runtime install"); + assert!(install.status.success()); + let install_json: Value = + serde_json::from_slice(install.stdout.as_slice()).expect("install json"); + assert_eq!(install_json["action"], "install"); + assert_eq!(install_json["state"], "configured"); + assert!( + install_json["detail"] + .as_str() + .expect("detail") + .contains(artifact_path.display().to_string().as_str()) + ); - assert_eq!(output.status.code(), Some(5)); - let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); - let json: Value = serde_json::from_str(stdout.as_str()).expect("json output"); - assert_eq!(json["action"], "install"); - assert_eq!(json["runtime_id"], "radrootsd"); - assert_eq!(json["state"], "deferred"); - assert_eq!(json["mutates_bindings"], false); - assert_eq!(json["next_step"], "rpv1-rpi.5"); + let registry_path = runtime_manager_registry_path(dir.path()); + let registry_raw = fs::read_to_string(&registry_path).expect("registry"); + assert!(registry_raw.contains("health_endpoint = \"http://127.0.0.1:7070\"")); + assert!(registry_raw.contains("secret_material_ref = ")); + + let start = runtime_command_in(dir.path()) + .args(["--json", "runtime", "start", "radrootsd"]) + .output() + .expect("runtime start"); + assert!(start.status.success()); + let start_json: Value = serde_json::from_slice(start.stdout.as_slice()).expect("start json"); + assert_eq!(start_json["state"], "running"); + + thread::sleep(Duration::from_millis(150)); + + let running_status = runtime_command_in(dir.path()) + .args(["--json", "runtime", "status", "radrootsd"]) + .output() + .expect("runtime status"); + assert!(running_status.status.success()); + let running_json: Value = + serde_json::from_slice(running_status.stdout.as_slice()).expect("status json"); + assert_eq!(running_json["state"], "configured"); + assert_eq!(running_json["health_state"], "running"); + + let config_set = runtime_command_in(dir.path()) + .args([ + "--json", + "runtime", + "config", + "set", + "radrootsd", + "config.rpc.addr", + "127.0.0.1:7444", + ]) + .output() + .expect("runtime config set"); + assert!(config_set.status.success()); + let config_set_json: Value = + serde_json::from_slice(config_set.stdout.as_slice()).expect("config set json"); + assert_eq!(config_set_json["state"], "configured"); + + let updated_registry_raw = fs::read_to_string(&registry_path).expect("updated registry"); + assert!(updated_registry_raw.contains("health_endpoint = \"http://127.0.0.1:7444\"")); + + let stop = runtime_command_in(dir.path()) + .args(["--json", "runtime", "stop", "radrootsd"]) + .output() + .expect("runtime stop"); + assert!(stop.status.success()); + let stop_json: Value = serde_json::from_slice(stop.stdout.as_slice()).expect("stop json"); + assert_eq!(stop_json["state"], "stopped"); + + let uninstall = runtime_command_in(dir.path()) + .args(["--json", "runtime", "uninstall", "radrootsd"]) + .output() + .expect("runtime uninstall"); + assert!(uninstall.status.success()); + let uninstall_json: Value = + serde_json::from_slice(uninstall.stdout.as_slice()).expect("uninstall json"); + assert_eq!(uninstall_json["state"], "uninstalled"); + + let final_status = runtime_command_in(dir.path()) + .args(["--json", "runtime", "status", "radrootsd"]) + .output() + .expect("runtime status after uninstall"); + assert!(final_status.status.success()); + let final_json: Value = + serde_json::from_slice(final_status.stdout.as_slice()).expect("final status json"); + assert_eq!(final_json["state"], "not_installed"); + assert_eq!(final_json["health_state"], "not_installed"); } #[test] @@ -139,14 +277,18 @@ fn runtime_logs_reports_managed_log_locations() { let json: Value = serde_json::from_str(stdout.as_str()).expect("json output"); assert_eq!(json["runtime_id"], "radrootsd"); assert_eq!(json["state"], "ready"); - assert!(json["stdout_log_path"] - .as_str() - .expect("stdout log path") - .ends_with("shared/runtime-manager/radrootsd/local/stdout.log")); - assert!(json["stderr_log_path"] - .as_str() - .expect("stderr log path") - .ends_with("shared/runtime-manager/radrootsd/local/stderr.log")); + assert!( + json["stdout_log_path"] + .as_str() + .expect("stdout log path") + .ends_with("shared/runtime-manager/radrootsd/local/stdout.log") + ); + assert!( + json["stderr_log_path"] + .as_str() + .expect("stderr log path") + .ends_with("shared/runtime-manager/radrootsd/local/stderr.log") + ); } #[test] @@ -154,12 +296,14 @@ fn runtime_config_show_uses_registered_instance_config_path() { let dir = tempdir().expect("tempdir"); let registry_path = runtime_manager_registry_path(dir.path()); let config_path = dir.path().join("managed").join("radrootsd-local.toml"); + let token_path = dir.path().join("managed").join("bridge-token.txt"); fs::create_dir_all(config_path.parent().expect("config parent")).expect("create config dir"); fs::write( &config_path, - "[config.rpc]\naddr = \"127.0.0.1:7070\"\n[config.bridge]\nenabled = true\nbearer_token = \"redacted\"\n", + "[metadata]\nname = \"managed-radrootsd\"\n[config.rpc]\naddr = \"127.0.0.1:7070\"\n[config.bridge]\nenabled = true\nbearer_token = \"redacted\"\n", ) .expect("write config"); + fs::write(&token_path, "redacted").expect("write token"); fs::create_dir_all(registry_path.parent().expect("registry parent")).expect("registry dir"); fs::write( &registry_path, @@ -177,11 +321,14 @@ config_path = "{config_path}" logs_path = "{logs_path}" run_path = "{run_path}" installed_version = "0.1.0" +health_endpoint = "http://127.0.0.1:7070" +secret_material_ref = "{secret_material_ref}" "#, binary_path = dir.path().join("bin/radrootsd").display(), config_path = config_path.display(), logs_path = dir.path().join("managed/logs/radrootsd-local").display(), run_path = dir.path().join("managed/run/radrootsd-local").display(), + secret_material_ref = token_path.display(), ), ) .expect("write registry"); diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs @@ -728,22 +728,13 @@ fn config_show_uses_managed_default_write_plane_when_local_instance_exists() { let registry_path = runtime_manager_registry_path(dir.path()); fs::create_dir_all(registry_path.parent().expect("registry parent")).expect("registry dir"); let managed_config_path = dir.path().join("managed-radrootsd.toml"); + let bridge_token_path = dir.path().join("managed-bridge-token.txt"); fs::write( &managed_config_path, - r#"[metadata] -name = "managed-radrootsd" - -[config] - -[config.rpc] -addr = "127.0.0.1:7444" - -[config.bridge] -enabled = true -bearer_token = "managed-bridge-token" -"#, + "[metadata]\nname = \"managed-radrootsd\"\n", ) .expect("write managed config"); + fs::write(&bridge_token_path, "managed-bridge-token").expect("write managed token"); fs::write( &registry_path, format!( @@ -760,8 +751,11 @@ config_path = "{}" logs_path = "/tmp/radrootsd/logs" run_path = "/tmp/radrootsd/run" installed_version = "0.1.0" +health_endpoint = "http://127.0.0.1:7444" +secret_material_ref = "{}" "#, - managed_config_path.display() + managed_config_path.display(), + bridge_token_path.display() ), ) .expect("write managed registry");