commit 4faedf460990e56c744578d615c25ca666a2cc26
parent e5a23a888b61202c04e930f255fb7ca5fd369ade
Author: triesap <tyson@radroots.org>
Date: Fri, 10 Apr 2026 22:48:00 +0000
runtime: manage local radrootsd lifecycle
Diffstat:
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(
®istry_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(®istry_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(®istry_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(
®istry_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(
®istry_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");