commit 432afc55df83633f43936c95dd5613c45a4e63c2
parent f194042783c39c7021a36963cd9698930e8c519d
Author: triesap <tyson@radroots.org>
Date: Sun, 23 Aug 2026 19:47:00 +0000
runtime-paths: adopt sealed RHI instance context
- replace worker and ambient path authority with typed service-instance roots
- derive exact common and encrypted-identity artifacts from RuntimeContext
- remove public path reports, path-leaf overrides, and implicit identity generation
- advance the dependency set coherently to the reachable final Lib revision
Diffstat:
22 files changed, 780 insertions(+), 2128 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1489,7 +1489,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radroots_blossom"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"mediatype",
"serde",
@@ -1501,7 +1501,7 @@ dependencies = [
[[package]]
name = "radroots_core"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"rust_decimal",
"serde",
@@ -1510,7 +1510,7 @@ dependencies = [
[[package]]
name = "radroots_event"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"hex",
"jiff-tzdb",
@@ -1528,7 +1528,7 @@ dependencies = [
[[package]]
name = "radroots_event_codec"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"hex",
"radroots_blossom",
@@ -1545,7 +1545,7 @@ dependencies = [
[[package]]
name = "radroots_identity"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"k256",
"serde",
@@ -1555,7 +1555,7 @@ dependencies = [
[[package]]
name = "radroots_nostr"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"nostr",
"radroots_event",
@@ -1569,15 +1569,24 @@ dependencies = [
[[package]]
name = "radroots_protocol"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"serde",
]
[[package]]
+name = "radroots_runtime_paths"
+version = "0.1.0-alpha"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
+dependencies = [
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
name = "radroots_secrets"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"chacha20poly1305",
"serde",
@@ -1589,7 +1598,7 @@ dependencies = [
[[package]]
name = "radroots_trade"
version = "0.1.0-alpha"
-source = "git+https://github.com/radrootslabs/lib?rev=09065a610d95e57acdc895a14c07580fa099e7c3#09065a610d95e57acdc895a14c07580fa099e7c3"
+source = "git+https://github.com/radrootslabs/lib?rev=7d7b454b4c9ed86569671993bd03ca868b676665#7d7b454b4c9ed86569671993bd03ca868b676665"
dependencies = [
"hex",
"radroots_core",
@@ -1750,6 +1759,7 @@ dependencies = [
"radroots_identity",
"radroots_nostr",
"radroots_protocol",
+ "radroots_runtime_paths",
"radroots_secrets",
"radroots_trade",
"rand 0.9.2",
diff --git a/Cargo.toml b/Cargo.toml
@@ -11,13 +11,14 @@ description = "Radroots trade agreement attestation worker"
resolver = "3"
[workspace.dependencies]
-radroots_event = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
-radroots_event_codec = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
-radroots_identity = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
-radroots_nostr = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
-radroots_protocol = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
-radroots_secrets = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
-radroots_trade = { git = "https://github.com/radrootslabs/lib", rev = "09065a610d95e57acdc895a14c07580fa099e7c3", version = "=0.1.0-alpha" }
+radroots_event = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_event_codec = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_identity = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_nostr = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_protocol = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_runtime_paths = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_secrets = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
+radroots_trade = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
[features]
default = []
@@ -31,6 +32,7 @@ radroots_event_codec = { workspace = true, features = ["json"] }
radroots_identity = { workspace = true }
radroots_nostr = { workspace = true, features = ["events"] }
radroots_protocol = { workspace = true }
+radroots_runtime_paths = { workspace = true }
radroots_secrets = { workspace = true }
radroots_trade = { workspace = true }
diff --git a/README b/README
@@ -21,9 +21,8 @@ instance, repo-local root, and config-path selection are CLI concerns and are
not document fields.
The current runtime loader and root `config.toml` remain transitional until
-their ordered replacement steps are complete. Existing environment and worker
-selectors are prototype evidence, not authority to change or weaken the v1
-contract.
+their ordered replacement steps are complete. Prototype environment and worker
+selectors are removed and are not compatibility authority.
`parse_rhi_config_v1` is the strict in-memory admission boundary for the target
document. It bounds original bytes before parsing, rejects malformed duplicate
@@ -57,6 +56,27 @@ database-path, worker, environment-file, or arbitrary path-leaf flag. Identity
rekey and replacement are not commands; rotation is a create-new offline
artifact plus governed configuration apply.
+## Sealed service-instance paths
+
+One validated CLI invocation resolves through `RhiRuntimeContext`, which owns
+the shared typed `RuntimeContext`, exact common artifacts, the validated
+`service.identity.ncrypt` credential path, and the explicit or canonical
+configuration selection. The service identity is fixed to `rhi`; the instance
+comes only from `InstanceId`; profile and repo-local-root provenance are the
+closed `bootstrap_cli` vocabulary. Callers cannot construct or mutate another
+path set, override artifact names, or obtain a public path report.
+
+Service-host roots are exactly
+`/etc/radroots/services/rhi/<instance>`,
+`/var/lib/radroots/services/rhi/<instance>`,
+`/var/cache/radroots/services/rhi/<instance>`,
+`/var/log/radroots/services/rhi/<instance>`,
+`/run/radroots/services/rhi/<instance>`, and
+`/etc/radroots/secrets/services/rhi/<instance>`. Interactive roots consume the
+injected host environment defined by `radroots_runtime_paths`; repo-local uses
+one explicit absolute base and the same `services/rhi/<instance>` namespace.
+Path resolution performs no directory creation or filesystem I/O.
+
Use the repository-owned Nix lanes for validation:
```text
diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml
@@ -1,9 +1,9 @@
schema = "radroots.lib.source-lock.v1"
repository = "https://github.com/radrootslabs/lib"
-revision = "09065a610d95e57acdc895a14c07580fa099e7c3"
+revision = "7d7b454b4c9ed86569671993bd03ca868b676665"
architecture = "radroots.crates.release.v2"
-workspace_catalog_sha256 = "8c27cebf6825f9ed74e122513c661f6dd31837dddb39d0988014121d8ab05e75"
+workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4"
version = "0.1.0-alpha"
-source_archive_sha256 = "68badd1fb02d9396682d368e62dfe79969d8da48529760789c823fb4ab54aea3"
+source_archive_sha256 = "b425371c134be96cce46b37f7035d6212f1efe8cff50bef366631ba5632991b0"
lockfile = "Cargo.lock"
-lockfile_sha256 = "3dda0cfcb08b133439343585c17429474cac1657dd97a7d46bfca3b04b3a8a87"
+lockfile_sha256 = "42a3f375556257d05db86bbeaa59d3a96f040afaa85b4293f238a7da98d95fde"
diff --git a/src/cli.rs b/src/cli.rs
@@ -1,30 +0,0 @@
-use crate::host_runtime::ServiceCliArgs;
-use clap::Parser;
-use std::path::PathBuf;
-
-#[derive(Parser, Debug, Clone)]
-#[command(
- about = env!("CARGO_PKG_DESCRIPTION"),
- author = env!("CARGO_PKG_AUTHORS"),
- version = env!("CARGO_PKG_VERSION")
-)]
-pub struct Args {
- #[command(subcommand)]
- pub command: Option<Command>,
- #[command(flatten)]
- pub service: ServiceCliArgs,
-}
-
-#[derive(clap::Subcommand, Debug, Clone)]
-pub enum Command {
- #[command(
- name = "attestation-smoke",
- about = "Run a release-product agreement attestation smoke request"
- )]
- AttestationSmoke {
- #[arg(long)]
- input: Option<PathBuf>,
- #[arg(long)]
- output: Option<PathBuf>,
- },
-}
diff --git a/src/cli_v1.rs b/src/cli_v1.rs
@@ -6,9 +6,7 @@ use std::fmt;
use std::path::{Component, Path, PathBuf};
use clap::{Parser, Subcommand, ValueEnum};
-
-/// Maximum encoded length of an RHI instance identifier.
-pub const RHI_INSTANCE_ID_MAX_BYTES: usize = 128;
+use radroots_runtime_paths::InstanceId;
/// The exact bootstrap profile selected by the operator.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -178,7 +176,7 @@ impl Error for RhiCliV1Error {}
/// A validated one-pass RHI bootstrap and command selection.
pub struct RhiCliInvocationV1 {
profile: RhiBootstrapProfileV1,
- instance: Box<str>,
+ instance: InstanceId,
repo_local_root: Option<PathBuf>,
config_path: Option<PathBuf>,
output_mode: RhiCliOutputModeV1,
@@ -194,7 +192,7 @@ impl RhiCliInvocationV1 {
/// Returns the validated instance identifier.
#[must_use]
- pub fn instance(&self) -> &str {
+ pub const fn instance(&self) -> &InstanceId {
&self.instance
}
@@ -262,7 +260,8 @@ where
let instance = parsed
.instance
.ok_or_else(|| RhiCliV1Error::new(RhiCliV1ErrorKind::InvalidArguments))?;
- validate_instance(&instance)?;
+ let instance = InstanceId::new(instance)
+ .map_err(|_| RhiCliV1Error::new(RhiCliV1ErrorKind::InvalidInstance))?;
validate_bootstrap_paths(
profile,
parsed.repo_local_root.as_deref(),
@@ -271,7 +270,7 @@ where
Ok(RhiCliInvocationV1 {
profile,
- instance: instance.into_boxed_str(),
+ instance,
repo_local_root: parsed.repo_local_root,
config_path: parsed.config,
output_mode: parsed.output.into(),
@@ -279,22 +278,6 @@ where
})
}
-fn validate_instance(value: &str) -> Result<(), RhiCliV1Error> {
- let bytes = value.as_bytes();
- let is_boundary = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
- if bytes.is_empty()
- || bytes.len() > RHI_INSTANCE_ID_MAX_BYTES
- || !is_boundary(bytes[0])
- || !is_boundary(bytes[bytes.len() - 1])
- || !bytes
- .iter()
- .all(|byte| is_boundary(*byte) || matches!(*byte, b'-' | b'_'))
- {
- return Err(RhiCliV1Error::new(RhiCliV1ErrorKind::InvalidInstance));
- }
- Ok(())
-}
-
fn validate_bootstrap_paths(
profile: RhiBootstrapProfileV1,
repo_local_root: Option<&Path>,
@@ -652,7 +635,7 @@ mod tests {
"run",
])
.expect("host profile");
- assert_eq!(invocation.instance(), "north-01");
+ assert_eq!(invocation.instance().as_str(), "north-01");
assert_eq!(
invocation.config_path(),
Some(Path::new("/etc/radroots/rhi.toml"))
@@ -696,7 +679,7 @@ mod tests {
assert_eq!(error.kind(), RhiCliV1ErrorKind::InvalidInstance);
}
- let exact = "a".repeat(RHI_INSTANCE_ID_MAX_BYTES);
+ let exact = "a".repeat(radroots_runtime_paths::INSTANCE_ID_MAX_BYTES);
assert!(
parse_rhi_cli_v1_from([
"rhi",
@@ -708,7 +691,7 @@ mod tests {
])
.is_ok()
);
- let overlong = "a".repeat(RHI_INSTANCE_ID_MAX_BYTES + 1);
+ let overlong = "a".repeat(radroots_runtime_paths::INSTANCE_ID_MAX_BYTES + 1);
assert_eq!(
parse_rhi_cli_v1_from([
"rhi",
diff --git a/src/config.rs b/src/config.rs
@@ -1,13 +1,14 @@
-use crate::host_nostr::Metadata;
-use crate::host_runtime::{BackoffConfig, NostrServiceConfig};
+//! Transitional runtime settings materialization under the sealed path context.
+
+use std::path::{Path, PathBuf};
+
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
-use std::path::{Path, PathBuf};
+use crate::RhiRuntimeContext;
use crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy;
-use crate::paths::{
- RhiRuntimePaths, default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
-};
+use crate::host_nostr::Metadata;
+use crate::host_runtime::{BackoffConfig, NostrServiceConfig};
fn default_replay_window_secs() -> u64 {
24 * 60 * 60
@@ -21,7 +22,7 @@ fn default_logging_filter() -> String {
"info".to_owned()
}
-fn default_logging_stdout() -> bool {
+const fn default_logging_stdout() -> bool {
true
}
@@ -35,21 +36,19 @@ pub struct LoggingConfig {
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default, deny_unknown_fields)]
struct RawLoggingConfig {
- pub output_dir: Option<PathBuf>,
- pub filter: Option<String>,
- pub stdout: Option<bool>,
+ filter: Option<String>,
+ stdout: Option<bool>,
}
impl RawLoggingConfig {
- fn into_logging_config(self, paths: &RhiRuntimePaths) -> Result<LoggingConfig> {
+ fn into_logging_config(self, context: &RhiRuntimeContext) -> Result<LoggingConfig> {
let filter = self.filter.unwrap_or_else(default_logging_filter);
let filter = filter.trim();
if filter.is_empty() {
bail!("logging.filter must not be empty");
}
-
Ok(LoggingConfig {
- output_dir: self.output_dir.unwrap_or_else(|| paths.logs_dir.clone()),
+ output_dir: context.context().paths().logs().to_path_buf(),
filter: filter.to_owned(),
stdout: self.stdout.unwrap_or_else(default_logging_stdout),
})
@@ -59,27 +58,27 @@ impl RawLoggingConfig {
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default, deny_unknown_fields)]
struct RawRelaysConfig {
- pub urls: Vec<String>,
+ urls: Vec<String>,
}
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default, deny_unknown_fields)]
struct RawNostrConfig {
- pub nip89: RawNip89Config,
+ nip89: RawNip89Config,
}
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default, deny_unknown_fields)]
struct RawNip89Config {
- pub identifier: Option<String>,
- pub extra_tags: Vec<Vec<String>>,
+ identifier: Option<String>,
+ extra_tags: Vec<Vec<String>>,
}
#[derive(Debug, Clone)]
struct RawServiceConfig {
- pub logging: LoggingConfig,
- pub relays: RawRelaysConfig,
- pub nostr: RawNostrConfig,
+ logging: LoggingConfig,
+ relays: RawRelaysConfig,
+ nostr: RawNostrConfig,
}
impl RawServiceConfig {
@@ -98,34 +97,29 @@ pub struct Configuration {
#[serde(flatten)]
pub service: NostrServiceConfig,
pub logging: LoggingConfig,
- #[serde(default)]
pub subscriber: SubscriberConfig,
#[serde(default)]
pub trade_agreement_attestation: TradeAgreementAttestationPolicy,
}
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriberConfig {
- #[serde(default)]
pub backoff: BackoffConfig,
- #[serde(default)]
pub state: SubscriberStateConfig,
}
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(default, deny_unknown_fields)]
struct RawSubscriberConfig {
- #[serde(default)]
- pub backoff: BackoffConfig,
- #[serde(default)]
- pub state: RawSubscriberStateConfig,
+ backoff: BackoffConfig,
+ state: RawSubscriberStateConfig,
}
impl RawSubscriberConfig {
- fn into_subscriber_config(self, paths: &RhiRuntimePaths) -> SubscriberConfig {
+ fn into_subscriber_config(self, context: &RhiRuntimeContext) -> SubscriberConfig {
SubscriberConfig {
backoff: self.backoff,
- state: self.state.into_subscriber_state_config(paths),
+ state: self.state.into_subscriber_state_config(context),
}
}
}
@@ -140,18 +134,15 @@ pub struct SubscriberStateConfig {
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
struct RawSubscriberStateConfig {
- #[serde(default)]
- pub path: Option<PathBuf>,
#[serde(default = "default_replay_window_secs")]
- pub replay_window_secs: u64,
+ replay_window_secs: u64,
#[serde(default = "default_replay_overlap_secs")]
- pub replay_overlap_secs: u64,
+ replay_overlap_secs: u64,
}
impl Default for RawSubscriberStateConfig {
fn default() -> Self {
Self {
- path: None,
replay_window_secs: default_replay_window_secs(),
replay_overlap_secs: default_replay_overlap_secs(),
}
@@ -159,60 +150,51 @@ impl Default for RawSubscriberStateConfig {
}
impl RawSubscriberStateConfig {
- fn into_subscriber_state_config(self, paths: &RhiRuntimePaths) -> SubscriberStateConfig {
+ fn into_subscriber_state_config(self, context: &RhiRuntimeContext) -> SubscriberStateConfig {
SubscriberStateConfig {
- path: self
- .path
- .unwrap_or_else(|| paths.subscriber_state_path.clone()),
+ path: context
+ .context()
+ .paths()
+ .state()
+ .join("trade-agreement-attestation")
+ .join("state.json"),
replay_window_secs: self.replay_window_secs,
replay_overlap_secs: self.replay_overlap_secs,
}
}
}
-impl Default for SubscriberStateConfig {
- fn default() -> Self {
- Self {
- path: default_subscriber_state_path_for_process()
- .expect("resolve canonical rhi subscriber state path"),
- replay_window_secs: default_replay_window_secs(),
- replay_overlap_secs: default_replay_overlap_secs(),
- }
- }
-}
-
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
struct RawSettings {
- pub metadata: Metadata,
+ metadata: Metadata,
#[serde(default)]
- pub logging: RawLoggingConfig,
+ logging: RawLoggingConfig,
#[serde(default)]
- pub relays: RawRelaysConfig,
+ relays: RawRelaysConfig,
#[serde(default)]
- pub nostr: RawNostrConfig,
+ nostr: RawNostrConfig,
#[serde(default)]
- pub subscriber: RawSubscriberConfig,
+ subscriber: RawSubscriberConfig,
#[serde(default)]
- pub trade_agreement_attestation: TradeAgreementAttestationPolicy,
+ trade_agreement_attestation: TradeAgreementAttestationPolicy,
}
impl RawSettings {
- fn into_settings(self, paths: &RhiRuntimePaths) -> Result<Settings> {
- let logging = self.logging.into_logging_config(paths)?;
+ fn into_settings(self, context: &RhiRuntimeContext) -> Result<Settings> {
+ let logging = self.logging.into_logging_config(context)?;
let service = RawServiceConfig {
logging: logging.clone(),
relays: self.relays,
nostr: self.nostr,
}
.into_service_config();
-
Ok(Settings {
metadata: self.metadata,
config: Configuration {
service,
logging,
- subscriber: self.subscriber.into_subscriber_config(paths),
+ subscriber: self.subscriber.into_subscriber_config(context),
trade_agreement_attestation: self.trade_agreement_attestation,
},
})
@@ -225,167 +207,62 @@ pub struct Settings {
pub config: Configuration,
}
-fn load_settings_from_path_with_resolver(
- path: &Path,
- resolver: &crate::host_paths::RadrootsPathResolver,
- profile: crate::host_paths::RadrootsPathProfile,
- repo_local_root: Option<&Path>,
-) -> Result<Settings> {
- let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?;
+/// Loads transitional runtime settings with all paths supplied by one sealed context.
+///
+/// The final versioned configuration parser is [`crate::parse_rhi_config_v1`].
+/// This adapter remains only until the legacy runtime is removed in Step 168;
+/// it accepts no path overrides and performs no ambient environment selection.
+pub fn load_settings_from_path(path: &Path, context: &RhiRuntimeContext) -> Result<Settings> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("read configuration from {}", path.display()))?;
let settings: RawSettings =
toml::from_str(&raw).with_context(|| format!("parse configuration {}", path.display()))?;
- let settings = settings.into_settings(&paths)?;
+ let settings = settings.into_settings(context)?;
settings.config.trade_agreement_attestation.validate()?;
Ok(settings)
}
-pub fn load_settings_from_path(path: &Path) -> Result<Settings> {
- let (profile, repo_local_root) = crate::paths::process_path_selection()?;
- load_settings_from_path_with_resolver(
- path,
- &crate::host_paths::RadrootsPathResolver::current(),
- profile,
- repo_local_root.as_deref(),
- )
-}
-
#[cfg(test)]
mod tests {
- use super::load_settings_from_path_with_resolver;
- use crate::features::trade_agreement_attestation::TradeAgreementAttestationBackend;
- use crate::host_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPlatform, RadrootsRuntimeNamespace,
- };
- use crate::paths::{
- default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
- runtime_contract_with_resolver,
- };
- use std::path::PathBuf;
-
- fn linux_resolver() -> RadrootsPathResolver {
- RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/treesap")),
- ..RadrootsHostEnvironment::default()
- },
- )
- }
-
- #[test]
- fn worker_namespace_uses_canonical_interactive_roots() {
- let namespace = RadrootsRuntimeNamespace::worker("rhi").expect("worker namespace");
- let namespaced = linux_resolver()
- .resolve(
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- )
- .expect("interactive_user roots")
- .namespaced(&namespace);
-
- assert_eq!(
- namespaced.config,
- PathBuf::from("/home/treesap/.radroots/config/workers/rhi")
- );
- assert_eq!(
- namespaced.data,
- PathBuf::from("/home/treesap/.radroots/data/workers/rhi")
- );
- assert_eq!(
- namespaced.logs,
- PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
- );
- assert_eq!(
- namespaced.secrets,
- PathBuf::from("/home/treesap/.radroots/secrets/workers/rhi")
- );
- }
-
- #[test]
- fn runtime_paths_follow_interactive_user_contract() {
- let paths = resolve_runtime_paths_with_resolver(
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
- )
- .expect("interactive_user paths should resolve");
-
- assert_eq!(
- paths.config_path,
- PathBuf::from("/home/treesap/.radroots/config/workers/rhi/config.toml")
- );
- assert_eq!(
- paths.logs_dir,
- PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
- );
- assert_eq!(
- paths.identity_path,
- PathBuf::from("/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json")
- );
- assert_eq!(
- paths.subscriber_state_path,
- PathBuf::from(
- "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json"
- )
- );
- }
-
- #[test]
- fn runtime_paths_follow_service_host_contract() {
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
- let paths =
- resolve_runtime_paths_with_resolver(&resolver, RadrootsPathProfile::ServiceHost, None)
- .expect("service_host paths should resolve");
+ use std::path::{Path, PathBuf};
- assert_eq!(
- paths.config_path,
- PathBuf::from("/etc/radroots/workers/rhi/config.toml")
- );
- assert_eq!(
- paths.logs_dir,
- PathBuf::from("/var/log/radroots/workers/rhi")
- );
- assert_eq!(
- paths.identity_path,
- PathBuf::from("/etc/radroots/secrets/workers/rhi/identity.secret.json")
- );
- assert_eq!(
- paths.subscriber_state_path,
- PathBuf::from("/var/lib/radroots/workers/rhi/trade-agreement-attestation/state.json")
- );
- }
+ use crate::{
+ RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from,
+ resolve_rhi_runtime_context,
+ };
- #[test]
- fn runtime_paths_follow_repo_local_contract() {
- let repo_local_root = PathBuf::from("/repo/.local/radroots/dev/rhi");
- let paths = resolve_runtime_paths_with_resolver(
- &linux_resolver(),
- RadrootsPathProfile::RepoLocal,
- Some(repo_local_root.as_path()),
+ use super::load_settings_from_path;
+
+ fn context() -> crate::RhiRuntimeContext {
+ let invocation = parse_rhi_cli_v1_from([
+ "rhi",
+ "--profile",
+ "interactive",
+ "--instance",
+ "default",
+ "run",
+ ])
+ .expect("invocation");
+ resolve_rhi_runtime_context(
+ &RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/operator")),
+ xdg_config_home: Some(PathBuf::from("/xdg/config")),
+ xdg_data_home: Some(PathBuf::from("/xdg/data")),
+ xdg_state_home: Some(PathBuf::from("/xdg/state")),
+ xdg_cache_home: Some(PathBuf::from("/xdg/cache")),
+ xdg_runtime_dir: Some(PathBuf::from("/xdg/run")),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
+ &invocation,
)
- .expect("repo_local paths should resolve");
-
- assert_eq!(
- paths.config_path,
- repo_local_root.join("config/workers/rhi/config.toml")
- );
- assert_eq!(paths.logs_dir, repo_local_root.join("logs/workers/rhi"));
- assert_eq!(
- paths.identity_path,
- repo_local_root.join("secrets/workers/rhi/identity.secret.json")
- );
- assert_eq!(
- paths.subscriber_state_path,
- repo_local_root.join("data/workers/rhi/trade-agreement-attestation/state.json")
- );
+ .expect("context")
}
#[test]
- fn load_settings_materializes_profile_defaults_when_paths_are_omitted() {
+ fn materializes_only_context_derived_paths() {
let temp = tempfile::tempdir().expect("tempdir");
let config_path = temp.path().join("config.toml");
std::fs::write(
@@ -397,378 +274,41 @@ name = "rhi-test"
[relays]
urls = ["wss://relay.example.com"]
-[nostr.nip89]
-identifier = "rhi"
-
[subscriber.state]
replay_window_secs = 123
replay_overlap_secs = 45
"#,
)
- .expect("write config");
-
- let settings = load_settings_from_path_with_resolver(
- &config_path,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
- )
- .expect("load settings");
-
- assert_eq!(
- settings.config.service.logs_dir,
- "/home/treesap/.radroots/logs/workers/rhi"
- );
+ .expect("config");
+ let settings = load_settings_from_path(&config_path, &context()).expect("settings");
assert_eq!(
settings.config.logging.output_dir,
- PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
- );
- assert_eq!(settings.config.logging.filter, "info");
- assert!(settings.config.logging.stdout);
- assert_eq!(
- settings.config.service.relays,
- vec!["wss://relay.example.com"]
- );
- assert_eq!(
- settings.config.service.nip89_identifier.as_deref(),
- Some("rhi")
+ Path::new("/xdg/state/radroots/logs/services/rhi/default")
);
assert_eq!(
settings.config.subscriber.state.path,
- PathBuf::from(
- "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json"
+ Path::new(
+ "/xdg/data/radroots/services/rhi/default/trade-agreement-attestation/state.json"
)
);
assert_eq!(settings.config.subscriber.state.replay_window_secs, 123);
assert_eq!(settings.config.subscriber.state.replay_overlap_secs, 45);
- assert_eq!(
- settings.config.trade_agreement_attestation.backend,
- TradeAgreementAttestationBackend::LocalStatementHash
- );
- }
-
- #[test]
- fn load_settings_parses_trade_agreement_attestation_policy() {
- let temp = tempfile::tempdir().expect("tempdir");
- let config_path = temp.path().join("config.toml");
- std::fs::write(
- &config_path,
- r#"
-[metadata]
-name = "rhi-test"
-
-[logging]
-output_dir = "logs/rhi"
-filter = "warn"
-stdout = false
-
-[relays]
-urls = ["wss://relay.example.com"]
-
-[nostr.nip89]
-identifier = "rhi"
-extra_tags = [["t", "radroots"]]
-
-[subscriber.backoff]
-base_ms = 10
-max_ms = 100
-factor = 3
-jitter_ms = 5
-
-[subscriber.state]
-path = "state/trade-agreement-attestation.json"
-
-[trade_agreement_attestation]
-backend = "local_statement_hash"
-expected_statement_contract_hash = "0x1111111111111111111111111111111111111111111111111111111111111111"
-validator_set_addr = "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde"
-validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
-"#,
- )
- .expect("write config");
-
- let settings = load_settings_from_path_with_resolver(
- &config_path,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
- )
- .expect("load settings");
-
- assert_eq!(settings.config.service.logs_dir, "logs/rhi");
- assert_eq!(
- settings.config.logging.output_dir,
- PathBuf::from("logs/rhi")
- );
- assert_eq!(settings.config.logging.filter, "warn");
- assert!(!settings.config.logging.stdout);
- assert_eq!(
- settings.config.service.relays,
- vec!["wss://relay.example.com"]
- );
- assert_eq!(
- settings.config.service.nip89_identifier.as_deref(),
- Some("rhi")
- );
- assert_eq!(
- settings.config.service.nip89_extra_tags,
- vec![vec!["t".to_owned(), "radroots".to_owned()]]
- );
- assert_eq!(settings.config.subscriber.backoff.base_ms, 10);
- assert_eq!(settings.config.subscriber.backoff.max_ms, 100);
- assert_eq!(settings.config.subscriber.backoff.factor, 3);
- assert_eq!(settings.config.subscriber.backoff.jitter_ms, 5);
- assert_eq!(
- settings.config.subscriber.state.path,
- PathBuf::from("state/trade-agreement-attestation.json")
- );
- assert_eq!(
- settings.config.trade_agreement_attestation.backend,
- TradeAgreementAttestationBackend::LocalStatementHash
- );
- assert_eq!(
- settings
- .config
- .trade_agreement_attestation
- .expected_statement_contract_hash
- .as_deref(),
- Some("0x1111111111111111111111111111111111111111111111111111111111111111")
- );
- assert_eq!(
- settings
- .config
- .trade_agreement_attestation
- .validator_set_addr
- .as_deref(),
- Some(
- "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde"
- )
- );
- assert_eq!(
- settings
- .config
- .trade_agreement_attestation
- .validator_set_event_id
- .as_deref(),
- Some("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
- );
- }
-
- #[test]
- fn load_settings_rejects_invalid_statement_contract_hash() {
- let temp = tempfile::tempdir().expect("tempdir");
- let config_path = temp.path().join("config.toml");
- std::fs::write(
- &config_path,
- r#"
-[metadata]
-name = "rhi-test"
-
-[trade_agreement_attestation]
-backend = "local_statement_hash"
-expected_statement_contract_hash = "not-a-hash"
-"#,
- )
- .expect("write config");
-
- let error = load_settings_from_path_with_resolver(
- &config_path,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
- )
- .expect_err("invalid statement contract hash must fail");
- let message = format!("{error:#}");
- assert!(
- message.contains("invalid configured hash field"),
- "{message}"
- );
- }
-
- #[test]
- fn load_settings_rejects_partial_validator_set_binding() {
- let temp = tempfile::tempdir().expect("tempdir");
- let config_path = temp.path().join("config.toml");
- std::fs::write(
- &config_path,
- r#"
-[metadata]
-name = "rhi-test"
-
-[trade_agreement_attestation]
-backend = "local_statement_hash"
-validator_set_addr = "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde"
-"#,
- )
- .expect("write config");
-
- let error = load_settings_from_path_with_resolver(
- &config_path,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
- )
- .expect_err("partial validator-set binding must fail");
- let message = format!("{error:#}");
- assert!(
- message.contains("attestation policy is missing validator_set_event_id"),
- "{message}"
- );
}
#[test]
- fn old_config_roots_are_rejected() {
+ fn path_leaf_overrides_are_rejected() {
let temp = tempfile::tempdir().expect("tempdir");
- for (name, body, needle) in [
- (
- "config-root",
- r#"
-[metadata]
-name = "rhi-test"
-
-[config]
-relays = ["wss://relay.example.com"]
-"#,
- "unknown field `config`",
- ),
- (
- "config-subscriber-backoff",
- r#"
-[metadata]
-name = "rhi-test"
-
-[config.subscriber.backoff]
-base_ms = 10
-"#,
- "unknown field `config`",
- ),
- (
- "config-subscriber-state",
- r#"
-[metadata]
-name = "rhi-test"
-
-[config.subscriber.state]
-replay_window_secs = 10
-"#,
- "unknown field `config`",
- ),
- (
- "config-trade-agreement-attestation",
- r#"
-[metadata]
-name = "rhi-test"
-
-[config.trade_agreement_attestation]
-backend = "local_statement_hash"
-"#,
- "unknown field `config`",
- ),
+ for (name, extra) in [
+ ("logging", "[logging]\noutput_dir = \"/tmp/logs\"\n"),
+ ("state", "[subscriber.state]\npath = \"/tmp/state.json\"\n"),
] {
let config_path = temp.path().join(format!("{name}.toml"));
- std::fs::write(&config_path, body).expect("write config");
-
- let error = load_settings_from_path_with_resolver(
+ std::fs::write(
&config_path,
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
+ format!("[metadata]\nname = \"rhi-test\"\n\n{extra}"),
)
- .expect_err("old config root must fail");
- let message = format!("{error:#}");
- assert!(message.contains(needle), "{message}");
+ .expect("config");
+ assert!(load_settings_from_path(&config_path, &context()).is_err());
}
}
-
- #[test]
- fn default_subscriber_state_path_is_canonical_for_current_process() {
- let path =
- default_subscriber_state_path_for_process().expect("resolve current process defaults");
- assert!(path.ends_with("trade-agreement-attestation/state.json"));
- }
-
- #[test]
- fn runtime_contract_output_matches_interactive_user_contract() {
- let contract = runtime_contract_with_resolver(
- &linux_resolver(),
- RadrootsPathProfile::InteractiveUser,
- None,
- )
- .expect("interactive-user contract");
-
- assert_eq!(contract.active_profile, "interactive_user");
- assert_eq!(contract.path_overrides.profile_source, "caller");
- assert_eq!(contract.path_overrides.root_source, "host_defaults");
- assert_eq!(contract.path_overrides.repo_local_root, None);
- assert_eq!(contract.path_overrides.repo_local_root_source, None);
- assert_eq!(
- contract.path_overrides.subordinate_path_override_source,
- "config_artifact"
- );
- assert_eq!(
- contract.path_overrides.subordinate_path_override_keys,
- vec![
- "logging.output_dir".to_owned(),
- "subscriber.state.path".to_owned(),
- ]
- );
- assert_eq!(
- contract.allowed_profiles,
- vec![
- "interactive_user".to_owned(),
- "service_host".to_owned(),
- "repo_local".to_owned(),
- ]
- );
- assert_eq!(contract.default_shared_secret_backend, "encrypted_file");
- assert_eq!(
- contract.allowed_shared_secret_backends,
- vec!["encrypted_file".to_owned()]
- );
- assert_eq!(
- contract.canonical_config_path,
- PathBuf::from("/home/treesap/.radroots/config/workers/rhi/config.toml")
- );
- assert_eq!(
- contract.canonical_logs_dir,
- PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
- );
- assert_eq!(
- contract.canonical_identity_path,
- PathBuf::from("/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json")
- );
- assert_eq!(
- contract.canonical_subscriber_state_path,
- PathBuf::from(
- "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json"
- )
- );
- }
-
- #[test]
- fn runtime_contract_output_matches_service_host_contract() {
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
- let contract =
- runtime_contract_with_resolver(&resolver, RadrootsPathProfile::ServiceHost, None)
- .expect("service-host contract");
-
- assert_eq!(contract.active_profile, "service_host");
- assert_eq!(
- contract.canonical_config_path,
- PathBuf::from("/etc/radroots/workers/rhi/config.toml")
- );
- assert_eq!(
- contract.canonical_logs_dir,
- PathBuf::from("/var/log/radroots/workers/rhi")
- );
- assert_eq!(
- contract.canonical_identity_path,
- PathBuf::from("/etc/radroots/secrets/workers/rhi/identity.secret.json")
- );
- assert_eq!(
- contract.canonical_subscriber_state_path,
- PathBuf::from("/var/lib/radroots/workers/rhi/trade-agreement-attestation/state.json")
- );
- }
}
diff --git a/src/features/trade_agreement_attestation.rs b/src/features/trade_agreement_attestation.rs
@@ -245,8 +245,9 @@ pub enum TradeAgreementAttestationRuntimeError {
impl Default for TradeAgreementAttestationRuntimeConfig {
fn default() -> Self {
Self {
- state_path: crate::paths::default_subscriber_state_path_for_process()
- .expect("resolve canonical rhi agreement-attestation state path"),
+ // In-memory runtimes never construct persistence from this value.
+ // Persistent runtimes must receive their context-derived path.
+ state_path: PathBuf::new(),
replay_window_secs: 24 * 60 * 60,
replay_overlap_secs: 5 * 60,
}
@@ -774,25 +775,6 @@ pub struct TradeAgreementAttestationSmokeResponse {
pub error: Option<String>,
}
-pub async fn run_smoke_cli_command(command: crate::cli::Command) -> anyhow::Result<()> {
- let crate::cli::Command::AttestationSmoke { input, output } = command;
- let request_bytes = read_input(input.as_deref())?;
- let response = handle_smoke_request_bytes(&request_bytes).await;
- let response_bytes = serde_json::to_vec_pretty(&response)?;
- write_output(output.as_deref(), &response_bytes)?;
- if response.ok {
- Ok(())
- } else {
- Err(anyhow!(
- "{}",
- response
- .error
- .as_deref()
- .unwrap_or("attestation smoke request failed")
- ))
- }
-}
-
pub async fn handle_smoke_request_bytes(bytes: &[u8]) -> TradeAgreementAttestationSmokeResponse {
match serde_json::from_slice::<TradeAgreementAttestationSmokeRequest>(bytes) {
Ok(request) if request.protocol_id == RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID => {
@@ -831,25 +813,6 @@ pub async fn handle_smoke_request_bytes(bytes: &[u8]) -> TradeAgreementAttestati
}
}
-fn read_input(path: Option<&Path>) -> anyhow::Result<Vec<u8>> {
- match path {
- Some(path) => std::fs::read(path).map_err(anyhow::Error::from),
- None => std::io::read_to_string(std::io::stdin())
- .map(|value| value.into_bytes())
- .map_err(anyhow::Error::from),
- }
-}
-
-fn write_output(path: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> {
- match path {
- Some(path) => std::fs::write(path, bytes).map_err(anyhow::Error::from),
- None => {
- println!("{}", String::from_utf8_lossy(bytes));
- Ok(())
- }
- }
-}
-
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
diff --git a/src/host_paths/error.rs b/src/host_paths/error.rs
@@ -1,35 +0,0 @@
-use thiserror::Error;
-
-use std::path::PathBuf;
-
-use super::{RadrootsPathProfile, RadrootsPlatform};
-
-#[derive(Debug, Error, Clone, PartialEq, Eq)]
-pub enum RadrootsRuntimePathsError {
- #[error("interactive_user on {platform} requires a home directory")]
- MissingHomeDir { platform: RadrootsPlatform },
-
- #[error("interactive_user on windows requires APPDATA and LOCALAPPDATA roots")]
- MissingWindowsUserDirs,
-
- #[error("service_host on windows requires a ProgramData root")]
- MissingWindowsProgramDataDir,
-
- #[error("repo_local requires an explicit repo-local base root")]
- MissingRepoLocalRoot,
-
- #[error("mobile_native requires explicit logical roots")]
- MissingMobileRoots,
-
- #[error("{profile} is not supported on {platform}")]
- UnsupportedProfilePlatform {
- profile: RadrootsPathProfile,
- platform: RadrootsPlatform,
- },
-
- #[error("runtime namespace `{value}` must be one non-empty path component")]
- InvalidNamespaceComponent { value: String },
-
- #[error("shared accounts data root `{path:?}` has no parent shared data root")]
- SharedAccountsDataRootMissingParent { path: PathBuf },
-}
diff --git a/src/host_paths/mod.rs b/src/host_paths/mod.rs
@@ -1,11 +0,0 @@
-//! RHI-owned host path policy.
-
-mod error;
-mod namespace;
-mod platform;
-mod roots;
-
-pub use error::RadrootsRuntimePathsError;
-pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind};
-pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform};
-pub use roots::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths};
diff --git a/src/host_paths/namespace.rs b/src/host_paths/namespace.rs
@@ -1,148 +0,0 @@
-use std::path::PathBuf;
-
-use super::RadrootsRuntimePathsError;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsRuntimeNamespaceKind {
- App,
- Service,
- Worker,
- Shared,
-}
-
-impl RadrootsRuntimeNamespaceKind {
- #[must_use]
- pub fn path_segment(self) -> &'static str {
- match self {
- Self::App => "apps",
- Self::Service => "services",
- Self::Worker => "workers",
- Self::Shared => "shared",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsRuntimeNamespace {
- kind: RadrootsRuntimeNamespaceKind,
- value: String,
-}
-
-impl RadrootsRuntimeNamespace {
- pub fn app(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
- Self::new(RadrootsRuntimeNamespaceKind::App, value)
- }
-
- pub fn service(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
- Self::new(RadrootsRuntimeNamespaceKind::Service, value)
- }
-
- pub fn worker(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
- Self::new(RadrootsRuntimeNamespaceKind::Worker, value)
- }
-
- pub fn shared(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
- Self::new(RadrootsRuntimeNamespaceKind::Shared, value)
- }
-
- pub fn new(
- kind: RadrootsRuntimeNamespaceKind,
- value: impl Into<String>,
- ) -> Result<Self, RadrootsRuntimePathsError> {
- let value = value.into();
- validate_component(&value)?;
- Ok(Self { kind, value })
- }
-
- #[must_use]
- pub fn kind(&self) -> RadrootsRuntimeNamespaceKind {
- self.kind
- }
-
- #[must_use]
- pub fn value(&self) -> &str {
- self.value.as_str()
- }
-
- #[must_use]
- pub fn relative_path(&self) -> PathBuf {
- PathBuf::from(self.kind.path_segment()).join(self.value.as_str())
- }
-}
-
-fn validate_component(value: &str) -> Result<(), RadrootsRuntimePathsError> {
- let trimmed = value.trim();
- if trimmed.is_empty()
- || trimmed == "."
- || trimmed == ".."
- || trimmed.contains('/')
- || trimmed.contains('\\')
- {
- return Err(RadrootsRuntimePathsError::InvalidNamespaceComponent {
- value: value.to_owned(),
- });
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
-
- use super::RadrootsRuntimePathsError;
- use super::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind};
-
- #[test]
- fn namespace_kind_path_segments_are_canonical() {
- assert_eq!(RadrootsRuntimeNamespaceKind::App.path_segment(), "apps");
- assert_eq!(
- RadrootsRuntimeNamespaceKind::Service.path_segment(),
- "services"
- );
- assert_eq!(
- RadrootsRuntimeNamespaceKind::Worker.path_segment(),
- "workers"
- );
- assert_eq!(
- RadrootsRuntimeNamespaceKind::Shared.path_segment(),
- "shared"
- );
- }
-
- #[test]
- fn namespace_constructors_preserve_kind_and_value() {
- let app = RadrootsRuntimeNamespace::app("cli").expect("app namespace");
- assert_eq!(app.kind(), RadrootsRuntimeNamespaceKind::App);
- assert_eq!(app.value(), "cli");
- assert_eq!(app.relative_path(), PathBuf::from("apps/cli"));
-
- let service = RadrootsRuntimeNamespace::service("myc").expect("service namespace");
- assert_eq!(service.kind(), RadrootsRuntimeNamespaceKind::Service);
- assert_eq!(service.value(), "myc");
- assert_eq!(service.relative_path(), PathBuf::from("services/myc"));
-
- let worker = RadrootsRuntimeNamespace::worker("rhi").expect("worker namespace");
- assert_eq!(worker.kind(), RadrootsRuntimeNamespaceKind::Worker);
- assert_eq!(worker.value(), "rhi");
- assert_eq!(worker.relative_path(), PathBuf::from("workers/rhi"));
-
- let shared = RadrootsRuntimeNamespace::shared("runtime").expect("shared namespace");
- assert_eq!(shared.kind(), RadrootsRuntimeNamespaceKind::Shared);
- assert_eq!(shared.value(), "runtime");
- assert_eq!(shared.relative_path(), PathBuf::from("shared/runtime"));
- }
-
- #[test]
- fn namespace_validation_rejects_invalid_components() {
- for invalid in ["", " ", ".", "..", "a/b", r"a\b"] {
- let err = RadrootsRuntimeNamespace::new(RadrootsRuntimeNamespaceKind::App, invalid)
- .expect_err("invalid namespace component should fail");
- assert_eq!(
- err,
- RadrootsRuntimePathsError::InvalidNamespaceComponent {
- value: invalid.to_owned(),
- }
- );
- }
- }
-}
diff --git a/src/host_paths/platform.rs b/src/host_paths/platform.rs
@@ -1,179 +0,0 @@
-use std::fmt;
-use std::path::PathBuf;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsPlatform {
- Linux,
- Macos,
- Windows,
- Android,
- Ios,
-}
-
-impl RadrootsPlatform {
- #[must_use]
- #[cfg(target_os = "android")]
- pub fn current() -> Self {
- Self::Android
- }
-
- #[must_use]
- #[cfg(target_os = "ios")]
- pub fn current() -> Self {
- Self::Ios
- }
-
- #[must_use]
- #[cfg(target_os = "macos")]
- pub fn current() -> Self {
- Self::Macos
- }
-
- #[must_use]
- #[cfg(target_os = "windows")]
- pub fn current() -> Self {
- Self::Windows
- }
-
- #[must_use]
- #[cfg(all(
- not(target_os = "android"),
- not(target_os = "ios"),
- not(target_os = "macos"),
- not(target_os = "windows")
- ))]
- pub fn current() -> Self {
- Self::Linux
- }
-
- #[must_use]
- pub fn is_unix_like(self) -> bool {
- matches!(self, Self::Linux | Self::Macos)
- }
-}
-
-impl fmt::Display for RadrootsPlatform {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(match self {
- Self::Linux => "linux",
- Self::Macos => "macos",
- Self::Windows => "windows",
- Self::Android => "android",
- Self::Ios => "ios",
- })
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsPathProfile {
- InteractiveUser,
- ServiceHost,
- RepoLocal,
- MobileNative,
-}
-
-impl fmt::Display for RadrootsPathProfile {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(match self {
- Self::InteractiveUser => "interactive_user",
- Self::ServiceHost => "service_host",
- Self::RepoLocal => "repo_local",
- Self::MobileNative => "mobile_native",
- })
- }
-}
-
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-pub struct RadrootsHostEnvironment {
- pub home_dir: Option<PathBuf>,
- pub appdata_dir: Option<PathBuf>,
- pub localappdata_dir: Option<PathBuf>,
- pub programdata_dir: Option<PathBuf>,
-}
-
-impl RadrootsHostEnvironment {
- #[must_use]
- pub fn from_current_process() -> Self {
- Self {
- home_dir: std::env::var_os("HOME").map(PathBuf::from),
- appdata_dir: std::env::var_os("APPDATA").map(PathBuf::from),
- localappdata_dir: std::env::var_os("LOCALAPPDATA").map(PathBuf::from),
- programdata_dir: std::env::var_os("ProgramData").map(PathBuf::from),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
-
- use super::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform};
-
- #[test]
- fn current_matches_compiled_target_platform() {
- #[cfg(target_os = "android")]
- let expected = RadrootsPlatform::Android;
- #[cfg(target_os = "ios")]
- let expected = RadrootsPlatform::Ios;
- #[cfg(target_os = "macos")]
- let expected = RadrootsPlatform::Macos;
- #[cfg(target_os = "windows")]
- let expected = RadrootsPlatform::Windows;
- #[cfg(all(
- not(target_os = "android"),
- not(target_os = "ios"),
- not(target_os = "macos"),
- not(target_os = "windows")
- ))]
- let expected = RadrootsPlatform::Linux;
-
- assert_eq!(RadrootsPlatform::current(), expected);
- }
-
- #[test]
- fn unix_like_classification_is_explicit() {
- assert!(RadrootsPlatform::Linux.is_unix_like());
- assert!(RadrootsPlatform::Macos.is_unix_like());
- assert!(!RadrootsPlatform::Windows.is_unix_like());
- assert!(!RadrootsPlatform::Android.is_unix_like());
- assert!(!RadrootsPlatform::Ios.is_unix_like());
- }
-
- #[test]
- fn display_uses_canonical_labels() {
- assert_eq!(RadrootsPlatform::Linux.to_string(), "linux");
- assert_eq!(RadrootsPlatform::Macos.to_string(), "macos");
- assert_eq!(RadrootsPlatform::Windows.to_string(), "windows");
- assert_eq!(RadrootsPlatform::Android.to_string(), "android");
- assert_eq!(RadrootsPlatform::Ios.to_string(), "ios");
-
- assert_eq!(
- RadrootsPathProfile::InteractiveUser.to_string(),
- "interactive_user"
- );
- assert_eq!(RadrootsPathProfile::ServiceHost.to_string(), "service_host");
- assert_eq!(RadrootsPathProfile::RepoLocal.to_string(), "repo_local");
- assert_eq!(
- RadrootsPathProfile::MobileNative.to_string(),
- "mobile_native"
- );
- }
-
- #[test]
- fn host_environment_reads_current_process_variables() {
- let env = RadrootsHostEnvironment::from_current_process();
- assert_eq!(env.home_dir, std::env::var_os("HOME").map(PathBuf::from));
- assert_eq!(
- env.appdata_dir,
- std::env::var_os("APPDATA").map(PathBuf::from)
- );
- assert_eq!(
- env.localappdata_dir,
- std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
- );
- assert_eq!(
- env.programdata_dir,
- std::env::var_os("ProgramData").map(PathBuf::from)
- );
- }
-}
diff --git a/src/host_paths/roots.rs b/src/host_paths/roots.rs
@@ -1,315 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use super::{
- RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimeNamespace,
- RadrootsRuntimePathsError,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsPaths {
- pub config: PathBuf,
- pub data: PathBuf,
- pub cache: PathBuf,
- pub logs: PathBuf,
- pub run: PathBuf,
- pub secrets: PathBuf,
-}
-
-impl RadrootsPaths {
- #[must_use]
- pub fn from_base_root(base_root: impl AsRef<Path>) -> Self {
- let base_root = base_root.as_ref();
- Self {
- config: base_root.join("config"),
- data: base_root.join("data"),
- cache: base_root.join("cache"),
- logs: base_root.join("logs"),
- run: base_root.join("run"),
- secrets: base_root.join("secrets"),
- }
- }
-
- #[must_use]
- pub fn namespaced(&self, namespace: &RadrootsRuntimeNamespace) -> Self {
- let relative = namespace.relative_path();
- Self {
- config: self.config.join(&relative),
- data: self.data.join(&relative),
- cache: self.cache.join(&relative),
- logs: self.logs.join(&relative),
- run: self.run.join(&relative),
- secrets: self.secrets.join(relative),
- }
- }
-}
-
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-pub struct RadrootsPathOverrides {
- pub repo_local_root: Option<PathBuf>,
- pub mobile_roots: Option<RadrootsPaths>,
-}
-
-impl RadrootsPathOverrides {
- #[must_use]
- pub fn repo_local(base_root: impl Into<PathBuf>) -> Self {
- Self {
- repo_local_root: Some(base_root.into()),
- mobile_roots: None,
- }
- }
-
- #[must_use]
- pub fn mobile(roots: RadrootsPaths) -> Self {
- Self {
- repo_local_root: None,
- mobile_roots: Some(roots),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsPathResolver {
- platform: RadrootsPlatform,
- host_environment: RadrootsHostEnvironment,
-}
-
-impl RadrootsPathResolver {
- #[must_use]
- pub fn new(platform: RadrootsPlatform, host_environment: RadrootsHostEnvironment) -> Self {
- Self {
- platform,
- host_environment,
- }
- }
-
- #[must_use]
- pub fn current() -> Self {
- Self::new(
- RadrootsPlatform::current(),
- RadrootsHostEnvironment::from_current_process(),
- )
- }
-
- #[must_use]
- pub fn platform(&self) -> RadrootsPlatform {
- self.platform
- }
-
- pub fn resolve(
- &self,
- profile: RadrootsPathProfile,
- overrides: &RadrootsPathOverrides,
- ) -> Result<RadrootsPaths, RadrootsRuntimePathsError> {
- match profile {
- RadrootsPathProfile::InteractiveUser => self.resolve_interactive_user(),
- RadrootsPathProfile::ServiceHost => self.resolve_service_host(),
- RadrootsPathProfile::RepoLocal => overrides
- .repo_local_root
- .as_ref()
- .map(RadrootsPaths::from_base_root)
- .ok_or(RadrootsRuntimePathsError::MissingRepoLocalRoot),
- RadrootsPathProfile::MobileNative => match self.platform {
- RadrootsPlatform::Android | RadrootsPlatform::Ios => overrides
- .mobile_roots
- .clone()
- .ok_or(RadrootsRuntimePathsError::MissingMobileRoots),
- _ => Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform {
- profile,
- platform: self.platform,
- }),
- },
- }
- }
-
- fn resolve_interactive_user(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> {
- match self.platform {
- RadrootsPlatform::Linux | RadrootsPlatform::Macos => self
- .host_environment
- .home_dir
- .as_ref()
- .map(|home| RadrootsPaths::from_base_root(home.join(".radroots")))
- .ok_or(RadrootsRuntimePathsError::MissingHomeDir {
- platform: self.platform,
- }),
- RadrootsPlatform::Windows => {
- let appdata = self
- .host_environment
- .appdata_dir
- .as_ref()
- .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?;
- let localappdata = self
- .host_environment
- .localappdata_dir
- .as_ref()
- .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?;
- let config_root = appdata.join("Radroots");
- let local_root = localappdata.join("Radroots");
- Ok(RadrootsPaths {
- config: config_root.join("config"),
- data: local_root.join("data"),
- cache: local_root.join("cache"),
- logs: local_root.join("logs"),
- run: local_root.join("run"),
- secrets: config_root.join("secrets"),
- })
- }
- RadrootsPlatform::Android | RadrootsPlatform::Ios => {
- Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform {
- profile: RadrootsPathProfile::InteractiveUser,
- platform: self.platform,
- })
- }
- }
- }
-
- fn resolve_service_host(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> {
- match self.platform {
- RadrootsPlatform::Windows => {
- let programdata = self
- .host_environment
- .programdata_dir
- .as_ref()
- .ok_or(RadrootsRuntimePathsError::MissingWindowsProgramDataDir)?;
- let base = programdata.join("Radroots");
- Ok(RadrootsPaths {
- config: base.join("config"),
- data: base.join("data"),
- cache: base.join("cache"),
- logs: base.join("logs"),
- run: base.join("run"),
- secrets: base.join("secrets"),
- })
- }
- RadrootsPlatform::Linux | RadrootsPlatform::Macos => Ok(RadrootsPaths {
- config: PathBuf::from("/etc/radroots"),
- data: PathBuf::from("/var/lib/radroots"),
- cache: PathBuf::from("/var/cache/radroots"),
- logs: PathBuf::from("/var/log/radroots"),
- run: PathBuf::from("/run/radroots"),
- secrets: PathBuf::from("/etc/radroots/secrets"),
- }),
- RadrootsPlatform::Android | RadrootsPlatform::Ios => {
- Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform {
- profile: RadrootsPathProfile::ServiceHost,
- platform: self.platform,
- })
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
-
- use super::{
- RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimePathsError,
- };
- use super::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths};
-
- #[test]
- fn path_override_helpers_only_populate_their_owned_slot() {
- let repo_local = RadrootsPathOverrides::repo_local("/repo/.local/radroots");
- assert_eq!(
- repo_local.repo_local_root,
- Some(PathBuf::from("/repo/.local/radroots"))
- );
- assert!(repo_local.mobile_roots.is_none());
-
- let mobile_roots = RadrootsPaths::from_base_root("/sandbox");
- let mobile = RadrootsPathOverrides::mobile(mobile_roots.clone());
- assert!(mobile.repo_local_root.is_none());
- assert_eq!(mobile.mobile_roots, Some(mobile_roots));
- }
-
- #[test]
- fn resolver_current_uses_process_platform_and_environment() {
- let resolver = RadrootsPathResolver::current();
- assert_eq!(resolver.platform(), RadrootsPlatform::current());
- assert_eq!(
- resolver,
- RadrootsPathResolver::new(
- RadrootsPlatform::current(),
- RadrootsHostEnvironment::from_current_process()
- )
- );
- }
-
- #[test]
- fn mobile_profile_is_rejected_on_non_mobile_platforms() {
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
-
- let err = resolver
- .resolve(
- RadrootsPathProfile::MobileNative,
- &RadrootsPathOverrides::default(),
- )
- .expect_err("mobile profile should be rejected on linux");
-
- assert_eq!(
- err,
- RadrootsRuntimePathsError::UnsupportedProfilePlatform {
- profile: RadrootsPathProfile::MobileNative,
- platform: RadrootsPlatform::Linux,
- }
- );
- }
-
- #[test]
- fn interactive_user_is_rejected_on_mobile_platforms() {
- for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] {
- let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default());
- let err = resolver
- .resolve(
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- )
- .expect_err("interactive_user should be unsupported on mobile");
- assert_eq!(
- err,
- RadrootsRuntimePathsError::UnsupportedProfilePlatform {
- profile: RadrootsPathProfile::InteractiveUser,
- platform,
- }
- );
- }
- }
-
- #[test]
- fn service_host_windows_requires_programdata() {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Windows,
- RadrootsHostEnvironment::default(),
- );
-
- let err = resolver
- .resolve(
- RadrootsPathProfile::ServiceHost,
- &RadrootsPathOverrides::default(),
- )
- .expect_err("service_host on windows should require programdata");
-
- assert_eq!(err, RadrootsRuntimePathsError::MissingWindowsProgramDataDir);
- }
-
- #[test]
- fn service_host_is_rejected_on_mobile_platforms() {
- for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] {
- let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default());
- let err = resolver
- .resolve(
- RadrootsPathProfile::ServiceHost,
- &RadrootsPathOverrides::default(),
- )
- .expect_err("service_host should be unsupported on mobile");
- assert_eq!(
- err,
- RadrootsRuntimePathsError::UnsupportedProfilePlatform {
- profile: RadrootsPathProfile::ServiceHost,
- platform,
- }
- );
- }
- }
-}
diff --git a/src/host_runtime.rs b/src/host_runtime.rs
@@ -2,22 +2,10 @@
use core::future::Future;
use core::time::Duration;
-use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
-use clap::{ArgAction, Args, ValueHint};
use serde::{Deserialize, Serialize};
-#[derive(Args, Debug, Clone)]
-pub struct ServiceCliArgs {
- #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
- pub config: Option<PathBuf>,
- #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
- pub identity: Option<PathBuf>,
- #[arg(long, action = ArgAction::SetTrue)]
- pub allow_generate_identity: bool,
-}
-
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NostrServiceConfig {
pub logs_dir: String,
diff --git a/src/identity_storage.rs b/src/identity_storage.rs
@@ -35,23 +35,12 @@ pub fn encrypted_identity_key_path(path: impl AsRef<Path>) -> PathBuf {
encrypted_identity_wrapping_key_path(path)
}
-pub fn load_service_identity(
- path: Option<&Path>,
- allow_generate: bool,
-) -> Result<RadrootsIdentity, IdentityError> {
- let path = path.map(Path::to_path_buf).unwrap_or_else(|| {
- crate::paths::default_identity_path_for_process()
- .expect("resolve canonical rhi identity path")
- });
+pub fn load_service_identity(path: &Path) -> Result<RadrootsIdentity, IdentityError> {
+ let path = path.to_path_buf();
if path.exists() {
return load_encrypted_identity(path);
}
- if !allow_generate {
- return Err(IdentityError::GenerationNotAllowed(path));
- }
- let identity = RadrootsIdentity::generate();
- store_encrypted_identity(path, &identity)?;
- Ok(identity)
+ Err(IdentityError::GenerationNotAllowed(path))
}
struct RhiFileKeyWrapping {
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,25 +1,22 @@
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
pub mod adapters;
-pub mod cli;
mod cli_v1;
pub mod config;
mod config_v1;
pub mod features;
pub mod host_identity;
pub mod host_nostr;
-pub mod host_paths;
pub mod host_runtime;
pub mod identity_storage;
-pub mod paths;
pub mod rhi;
+mod runtime_context;
-pub use cli::Args as cli_args;
pub use cli_v1::{
- RHI_INSTANCE_ID_MAX_BYTES, RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1,
- RhiCliV1Error, RhiCliV1ErrorKind, RhiCommandV1, RhiConfigCommandV1, RhiIdentityCommandV1,
- RhiMetricsCommandV1, RhiPresenceCommandV1, RhiPublicationCommandV1, RhiReconciliationCommandV1,
- RhiSourcesCommandV1, RhiStateCommandV1, RhiTradeCommandV1, parse_rhi_cli_v1_from,
+ RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1, RhiCliV1Error,
+ RhiCliV1ErrorKind, RhiCommandV1, RhiConfigCommandV1, RhiIdentityCommandV1, RhiMetricsCommandV1,
+ RhiPresenceCommandV1, RhiPublicationCommandV1, RhiReconciliationCommandV1, RhiSourcesCommandV1,
+ RhiStateCommandV1, RhiTradeCommandV1, parse_rhi_cli_v1_from,
};
pub use config_v1::{
RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES, RHI_CONFIG_EFFECTIVE_MAX_UTF8_BYTES, RHI_CONFIG_SCHEMA,
@@ -27,6 +24,15 @@ pub use config_v1::{
RhiConfigV1Error, RhiConfigV1ErrorKind, RhiConfigValueSource, RhiEffectiveConfigV1,
RhiRuntimeThreadLimitsV1, parse_rhi_config_v1,
};
+pub use radroots_runtime_paths::{
+ INSTANCE_ID_MAX_BYTES, InstanceId, RadrootsHostEnvironment, RadrootsPathProfile,
+ RadrootsPathResolver, RadrootsPlatform, RadrootsServiceInstanceArtifacts, RuntimeContext,
+ RuntimeContextSource, ServiceId,
+};
+pub use runtime_context::{
+ RhiRuntimeContext, RhiRuntimeContextError, RhiRuntimeContextErrorKind,
+ resolve_rhi_runtime_context,
+};
use anyhow::{Context, Result, anyhow, bail};
use radroots_event::{
@@ -186,11 +192,8 @@ async fn wait_for_shutdown_or_stopped(handle: crate::rhi::RhiHandle) -> RunRhiWa
}
}
-pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> {
- let identity = load_service_identity(
- args.service.identity.as_deref(),
- args.service.allow_generate_identity,
- )?;
+pub async fn run_rhi(settings: &config::Settings, context: &RhiRuntimeContext) -> Result<()> {
+ let identity = load_service_identity(context.identity_path())?;
let keys = identity.keys().clone();
let agreement_attestation_runtime =
TradeAgreementAttestationRuntime::load(TradeAgreementAttestationRuntimeConfig {
@@ -279,9 +282,8 @@ mod tests {
bootstrap_presence, build_authored_service_profile_event, release_product_handler_kinds,
run_rhi, run_rhi_bootstrap_hook, run_rhi_wait_hook,
};
- use crate::{cli_args, config};
+ use crate::{config, parse_rhi_cli_v1_from, resolve_rhi_runtime_context};
use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS;
- use std::path::PathBuf;
use std::sync::atomic::Ordering;
use tokio::sync::{Mutex, MutexGuard};
@@ -300,7 +302,10 @@ mod tests {
guard
}
- fn settings_with_relays(relays: Vec<String>) -> config::Settings {
+ fn settings_with_relays(
+ relays: Vec<String>,
+ context: &crate::RhiRuntimeContext,
+ ) -> config::Settings {
config::Settings {
metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"),
config: config::Configuration {
@@ -326,8 +331,13 @@ mod tests {
jitter_ms: 0,
},
state: config::SubscriberStateConfig {
- path: unique_state_path("settings"),
- ..Default::default()
+ path: context
+ .context()
+ .paths()
+ .state()
+ .join("trade-agreement-attestation/state.json"),
+ replay_window_secs: 24 * 60 * 60,
+ replay_overlap_secs: 5 * 60,
},
},
trade_agreement_attestation:
@@ -336,43 +346,46 @@ mod tests {
}
}
- fn args_for_identity(path: PathBuf) -> cli_args {
- cli_args {
- command: None,
- service: crate::host_runtime::ServiceCliArgs {
- config: Some(PathBuf::from("config.toml")),
- identity: Some(path),
- allow_generate_identity: true,
- },
- }
- }
-
- fn unique_identity_path(suffix: &str) -> PathBuf {
- let nanos = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("time")
- .as_nanos();
- std::env::temp_dir().join(format!("rhi-{suffix}-{nanos}.secret.json"))
+ fn context_for_root(root: &std::path::Path) -> crate::RhiRuntimeContext {
+ let root = root.to_str().expect("UTF-8 temp root");
+ let invocation = parse_rhi_cli_v1_from([
+ "rhi",
+ "--profile",
+ "repo-local",
+ "--instance",
+ "default",
+ "--repo-local-root",
+ root,
+ "run",
+ ])
+ .expect("invocation");
+ resolve_rhi_runtime_context(
+ &crate::RadrootsPathResolver::new(
+ crate::RadrootsPlatform::Linux,
+ crate::RadrootsHostEnvironment::default(),
+ ),
+ &invocation,
+ )
+ .expect("context")
}
- fn unique_state_path(suffix: &str) -> PathBuf {
- let nanos = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("time")
- .as_nanos();
- std::env::temp_dir()
- .join(format!("rhi-state-{suffix}-{nanos}"))
- .join("state.json")
+ fn provision_identity(context: &crate::RhiRuntimeContext) {
+ crate::identity_storage::store_encrypted_identity(
+ context.identity_path(),
+ &crate::host_identity::RadrootsIdentity::generate(),
+ )
+ .expect("identity");
}
#[tokio::test]
async fn run_rhi_starts_and_stops_without_relays() {
let _guard = test_guard().await;
RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed);
- let identity_path = unique_identity_path("no-relays");
- let args = args_for_identity(identity_path);
- let settings = settings_with_relays(Vec::new());
- run_rhi(&settings, &args).await.expect("run rhi");
+ let temp = tempfile::tempdir().expect("tempdir");
+ let context = context_for_root(temp.path());
+ provision_identity(&context);
+ let settings = settings_with_relays(Vec::new(), &context);
+ run_rhi(&settings, &context).await.expect("run rhi");
}
#[tokio::test]
@@ -382,10 +395,11 @@ mod tests {
*run_rhi_bootstrap_hook()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Ok(()));
- let identity_path = unique_identity_path("relays");
- let args = args_for_identity(identity_path);
- let settings = settings_with_relays(vec!["wss://relay.example.com".to_string()]);
- run_rhi(&settings, &args).await.expect("run rhi");
+ let temp = tempfile::tempdir().expect("tempdir");
+ let context = context_for_root(temp.path());
+ provision_identity(&context);
+ let settings = settings_with_relays(vec!["wss://relay.example.com".to_string()], &context);
+ run_rhi(&settings, &context).await.expect("run rhi");
assert_eq!(release_product_handler_kinds(), TRADE_MUTATION_EVENT_KINDS);
}
@@ -395,10 +409,11 @@ mod tests {
*run_rhi_wait_hook()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Stopped);
- let identity_path = unique_identity_path("wait-hook");
- let args = args_for_identity(identity_path);
- let settings = settings_with_relays(Vec::new());
- run_rhi(&settings, &args).await.expect("run rhi");
+ let temp = tempfile::tempdir().expect("tempdir");
+ let context = context_for_root(temp.path());
+ provision_identity(&context);
+ let settings = settings_with_relays(Vec::new(), &context);
+ run_rhi(&settings, &context).await.expect("run rhi");
}
#[tokio::test]
diff --git a/src/main.rs b/src/main.rs
@@ -1,104 +1,57 @@
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
-#[cfg(not(test))]
-use anyhow::Context;
-use anyhow::Result;
-#[cfg(not(test))]
-use clap::Parser;
-#[cfg(not(test))]
-use rhi::cli::Command;
-#[cfg(not(test))]
-use rhi::features::trade_agreement_attestation::run_smoke_cli_command;
-use rhi::{cli_args, config, paths, run_rhi};
use std::path::PathBuf;
use std::process::ExitCode;
-use tracing::info;
-#[cfg(not(test))]
-#[tokio::main]
-async fn main() -> ExitCode {
- exit_code_from_run(run().await)
-}
+use anyhow::{Context, Result, bail};
+use rhi::{
+ RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiCommandV1,
+ RhiRuntimeContext, parse_rhi_cli_v1_from, resolve_rhi_runtime_context, run_rhi,
+};
-#[cfg(test)]
fn main() -> ExitCode {
- exit_code_from_run(Ok(()))
+ let invocation = match parse_rhi_cli_v1_from(std::env::args_os()) {
+ Ok(invocation) => invocation,
+ Err(_) => return ExitCode::FAILURE,
+ };
+ let runtime = match tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ {
+ Ok(runtime) => runtime,
+ Err(_) => return ExitCode::FAILURE,
+ };
+ exit_code_from_run(runtime.block_on(execute(invocation)))
}
fn exit_code_from_run(result: Result<()>) -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
- Err(err) => {
- tracing::error!(error = ?err, "Fatal error");
- eprintln!("Fatal error: {err:#}");
+ Err(_) => {
+ eprintln!("RHI command failed");
ExitCode::FAILURE
}
}
}
-#[cfg(test)]
-type RunLoadHookValue = Option<Result<(cli_args, config::Settings)>>;
-#[cfg(test)]
-type RunLoadHook = std::sync::Mutex<RunLoadHookValue>;
-#[cfg(test)]
-static RUN_LOAD_HOOK: std::sync::OnceLock<RunLoadHook> = std::sync::OnceLock::new();
-
-#[cfg(test)]
-fn run_load_hook() -> &'static RunLoadHook {
- RUN_LOAD_HOOK.get_or_init(|| std::sync::Mutex::new(None))
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct RhiRuntimeStartupReport {
- active_profile: String,
- config_path: PathBuf,
- config_path_source: String,
- canonical_config_path: PathBuf,
- logs_dir: PathBuf,
- logs_dir_source: String,
- canonical_logs_dir: PathBuf,
- identity_path: PathBuf,
- identity_path_source: String,
- canonical_identity_path: PathBuf,
- subscriber_state_path: PathBuf,
- subscriber_state_path_source: String,
- canonical_subscriber_state_path: PathBuf,
- path_overrides: paths::RhiRuntimePathOverrideContractOutput,
- default_shared_secret_backend: String,
- allowed_shared_secret_backends: Vec<String>,
-}
-
-fn load_args_and_settings() -> Result<(cli_args, config::Settings)> {
- #[cfg(test)]
- {
- if let Some(result) = run_load_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- .take()
- {
- return result;
- }
- Err(anyhow::anyhow!("run loader hook not set"))
+async fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<()> {
+ let resolver = RadrootsPathResolver::new(RadrootsPlatform::current(), host_environment());
+ let context = resolve_rhi_runtime_context(&resolver, &invocation)
+ .map_err(|_| anyhow::anyhow!("RHI runtime context is invalid"))?;
+ match invocation.command() {
+ RhiCommandV1::Run => execute_run(&context).await,
+ _ => bail!("RHI command execution is not available in this checkpoint"),
}
+}
- #[cfg(not(test))]
- {
- let args = cli_args::try_parse()?;
- let config_path = args
- .service
- .config
- .clone()
- .map(Ok)
- .unwrap_or_else(paths::default_config_path_for_process)?;
- let settings =
- config::load_settings_from_path(&config_path).context("load configuration")?;
- init_rhi_logging(&settings)?;
- Ok((args, settings))
- }
+async fn execute_run(context: &RhiRuntimeContext) -> Result<()> {
+ let settings = rhi::config::load_settings_from_path(context.selected_config_path(), context)
+ .context("load RHI configuration")?;
+ init_rhi_logging(&settings)?;
+ run_rhi(&settings, context).await
}
-#[cfg(not(test))]
-fn init_rhi_logging(settings: &config::Settings) -> Result<()> {
+fn init_rhi_logging(settings: &rhi::config::Settings) -> Result<()> {
use tracing_subscriber::fmt::writer::MakeWriterExt as _;
std::fs::create_dir_all(&settings.config.logging.output_dir)
@@ -113,391 +66,48 @@ fn init_rhi_logging(settings: &config::Settings) -> Result<()> {
.with_env_filter(filter)
.with_writer(writer.and(std::io::stdout))
.try_init()
- .map_err(|error| anyhow::anyhow!("initialize RHI logging: {error}"))?;
+ .map_err(|_| anyhow::anyhow!("initialize RHI logging"))?;
} else {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(writer)
.try_init()
- .map_err(|error| anyhow::anyhow!("initialize RHI logging: {error}"))?;
+ .map_err(|_| anyhow::anyhow!("initialize RHI logging"))?;
}
LOG_GUARD
.set(guard)
.map_err(|_| anyhow::anyhow!("RHI logging is already initialized"))
}
-fn runtime_startup_report(
- args: &cli_args,
- settings: &config::Settings,
- contract: &paths::RhiRuntimeContractOutput,
-) -> RhiRuntimeStartupReport {
- RhiRuntimeStartupReport {
- active_profile: contract.active_profile.clone(),
- config_path: args
- .service
- .config
- .clone()
- .unwrap_or_else(|| contract.canonical_config_path.clone()),
- config_path_source: cli_or_profile_path_source(
- args.service.config.is_some(),
- &args
- .service
- .config
- .clone()
- .unwrap_or_else(|| contract.canonical_config_path.clone()),
- &contract.canonical_config_path,
- ),
- canonical_config_path: contract.canonical_config_path.clone(),
- logs_dir: settings.config.logging.output_dir.clone(),
- logs_dir_source: config_or_profile_path_source(
- &settings.config.logging.output_dir,
- &contract.canonical_logs_dir,
- ),
- canonical_logs_dir: contract.canonical_logs_dir.clone(),
- identity_path: args
- .service
- .identity
- .clone()
- .unwrap_or_else(|| contract.canonical_identity_path.clone()),
- identity_path_source: cli_or_profile_path_source(
- args.service.identity.is_some(),
- &args
- .service
- .identity
- .clone()
- .unwrap_or_else(|| contract.canonical_identity_path.clone()),
- &contract.canonical_identity_path,
- ),
- canonical_identity_path: contract.canonical_identity_path.clone(),
- subscriber_state_path: settings.config.subscriber.state.path.clone(),
- subscriber_state_path_source: config_or_profile_path_source(
- &settings.config.subscriber.state.path,
- &contract.canonical_subscriber_state_path,
- ),
- canonical_subscriber_state_path: contract.canonical_subscriber_state_path.clone(),
- path_overrides: contract.path_overrides.clone(),
- default_shared_secret_backend: contract.default_shared_secret_backend.clone(),
- allowed_shared_secret_backends: contract.allowed_shared_secret_backends.clone(),
- }
-}
-
-fn cli_or_profile_path_source(
- is_cli_arg: bool,
- actual_path: &PathBuf,
- canonical_path: &PathBuf,
-) -> String {
- if is_cli_arg {
- "cli_arg".to_owned()
- } else {
- config_or_profile_path_source(actual_path, canonical_path)
- }
-}
-
-fn config_or_profile_path_source(actual_path: &PathBuf, canonical_path: &PathBuf) -> String {
- if actual_path == canonical_path {
- "profile_default".to_owned()
- } else {
- "config_artifact".to_owned()
- }
-}
-
-#[cfg(not(test))]
-fn log_runtime_startup_report(report: &RhiRuntimeStartupReport) {
- info!(
- active_profile = report.active_profile.as_str(),
- profile_source = report.path_overrides.profile_source.as_str(),
- root_source = report.path_overrides.root_source.as_str(),
- repo_local_root = ?report.path_overrides.repo_local_root,
- repo_local_root_source = ?report.path_overrides.repo_local_root_source,
- subordinate_path_override_source = report.path_overrides.subordinate_path_override_source.as_str(),
- config_path = %report.config_path.display(),
- config_path_source = report.config_path_source.as_str(),
- canonical_config_path = %report.canonical_config_path.display(),
- logs_dir = %report.logs_dir.display(),
- logs_dir_source = report.logs_dir_source.as_str(),
- canonical_logs_dir = %report.canonical_logs_dir.display(),
- identity_path = %report.identity_path.display(),
- identity_path_source = report.identity_path_source.as_str(),
- canonical_identity_path = %report.canonical_identity_path.display(),
- subscriber_state_path = %report.subscriber_state_path.display(),
- subscriber_state_path_source = report.subscriber_state_path_source.as_str(),
- canonical_subscriber_state_path = %report.canonical_subscriber_state_path.display(),
- default_shared_secret_backend = report.default_shared_secret_backend.as_str(),
- allowed_shared_secret_backends = ?report.allowed_shared_secret_backends,
- "rhi runtime contract"
- );
-}
-
-async fn run() -> Result<()> {
- #[cfg(not(test))]
- {
- let args = cli_args::try_parse()?;
- if let Some(command) = args.command {
- return match command {
- Command::AttestationSmoke { .. } => run_smoke_cli_command(command).await,
- };
- }
- }
-
- let (args, settings): (cli_args, config::Settings) = load_args_and_settings()?;
-
- #[cfg(not(test))]
- {
- let contract = paths::runtime_contract_for_process().context("resolve runtime contract")?;
- let report = runtime_startup_report(&args, &settings, &contract);
- log_runtime_startup_report(&report);
+fn host_environment() -> RadrootsHostEnvironment {
+ let path = |name| {
+ std::env::var_os(name)
+ .filter(|value| !value.is_empty())
+ .map(PathBuf::from)
+ };
+ RadrootsHostEnvironment {
+ home_dir: path("HOME"),
+ xdg_config_home: path("XDG_CONFIG_HOME"),
+ xdg_data_home: path("XDG_DATA_HOME"),
+ xdg_state_home: path("XDG_STATE_HOME"),
+ xdg_cache_home: path("XDG_CACHE_HOME"),
+ xdg_runtime_dir: path("XDG_RUNTIME_DIR"),
+ appdata_dir: path("APPDATA"),
+ localappdata_dir: path("LOCALAPPDATA"),
}
-
- info!("Starting");
-
- run_rhi(&settings, &args).await
}
#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
- use super::{
- RhiRuntimeStartupReport, exit_code_from_run, main, run, run_load_hook, run_rhi,
- runtime_startup_report,
- };
- use rhi::features::trade_agreement_attestation::TradeAgreementAttestationRuntime;
- use rhi::host_nostr::{Client, Keys};
- use rhi::{cli_args, config, paths};
- use std::path::PathBuf;
+ use super::exit_code_from_run;
use std::process::ExitCode;
- static RUN_HOOK_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
-
- async fn run_hook_test_guard() -> tokio::sync::MutexGuard<'static, ()> {
- RUN_HOOK_TEST_LOCK.lock().await
- }
-
- fn minimal_settings() -> config::Settings {
- config::Settings {
- metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"),
- config: config::Configuration {
- service: rhi::host_runtime::NostrServiceConfig {
- logs_dir: std::env::temp_dir()
- .join("rhi-test-logs")
- .display()
- .to_string(),
- relays: Vec::new(),
- nip89_identifier: Some("rhi".to_string()),
- nip89_extra_tags: Vec::new(),
- },
- logging: config::LoggingConfig {
- output_dir: std::env::temp_dir().join("rhi-test-logs"),
- filter: "info".to_string(),
- stdout: true,
- },
- subscriber: config::SubscriberConfig::default(),
- trade_agreement_attestation:
- rhi::features::trade_agreement_attestation::TradeAgreementAttestationPolicy::default(),
- },
- }
- }
-
- fn sample_runtime_contract() -> paths::RhiRuntimeContractOutput {
- paths::RhiRuntimeContractOutput {
- active_profile: "interactive_user".to_string(),
- allowed_profiles: vec![
- "interactive_user".to_string(),
- "service_host".to_string(),
- "repo_local".to_string(),
- ],
- path_overrides: paths::RhiRuntimePathOverrideContractOutput {
- profile_source: "caller".to_string(),
- root_source: "host_defaults".to_string(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "config_artifact".to_string(),
- subordinate_path_override_keys: vec![
- "logging.output_dir".to_string(),
- "subscriber.state.path".to_string(),
- ],
- },
- default_shared_secret_backend: "encrypted_file".to_string(),
- allowed_shared_secret_backends: vec!["encrypted_file".to_string()],
- canonical_config_path: PathBuf::from(
- "/home/treesap/.radroots/config/workers/rhi/config.toml",
- ),
- canonical_logs_dir: PathBuf::from("/home/treesap/.radroots/logs/workers/rhi"),
- canonical_identity_path: PathBuf::from(
- "/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json",
- ),
- canonical_subscriber_state_path: PathBuf::from(
- "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json",
- ),
- }
- }
-
#[test]
- fn exit_code_from_run_maps_success_and_error() {
+ fn process_result_is_stable() {
assert_eq!(exit_code_from_run(Ok(())), ExitCode::SUCCESS);
assert_eq!(
- exit_code_from_run(Err(anyhow::anyhow!("boom"))),
+ exit_code_from_run(Err(anyhow::anyhow!("secret path"))),
ExitCode::FAILURE
);
}
-
- #[tokio::test]
- async fn run_rhi_returns_error_when_identity_is_missing() {
- let args = cli_args {
- command: None,
- service: rhi::host_runtime::ServiceCliArgs {
- config: Some(PathBuf::from("config.toml")),
- identity: Some(PathBuf::from("/tmp/rhi-missing-identity.secret.json")),
- allow_generate_identity: false,
- },
- };
- let settings = minimal_settings();
- let err = run_rhi(&settings, &args)
- .await
- .expect_err("identity should fail");
- let msg = format!("{err:#}");
- assert!(msg.contains("identity"));
- }
-
- #[test]
- fn main_returns_success_in_test_build() {
- assert_eq!(main(), ExitCode::SUCCESS);
- }
-
- #[tokio::test]
- async fn run_uses_injected_config_loader_result() {
- let _guard = run_hook_test_guard().await;
- let args = cli_args {
- command: None,
- service: rhi::host_runtime::ServiceCliArgs {
- config: Some(PathBuf::from("config.toml")),
- identity: Some(PathBuf::from("/tmp/rhi-run-hook-missing.secret.json")),
- allow_generate_identity: false,
- },
- };
- *run_load_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) =
- Some(Ok((args, minimal_settings())));
- let err = run().await.expect_err("missing identity should bubble");
- let msg = format!("{err:#}");
- assert!(msg.contains("identity"));
- }
-
- #[tokio::test]
- async fn run_returns_error_when_loader_hook_is_absent() {
- let _guard = run_hook_test_guard().await;
- *run_load_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
- let err = run()
- .await
- .expect_err("loader hook should be required in test build");
- let msg = format!("{err:#}");
- assert!(msg.contains("run loader hook not set"));
- }
-
- #[tokio::test]
- async fn non_test_start_subscriber_path_can_start_and_stop() {
- let keys = Keys::generate();
- let client = Client::new(keys.clone());
- let handle = rhi::rhi::start_subscriber(
- client,
- keys,
- TradeAgreementAttestationRuntime::new(),
- rhi::host_runtime::BackoffConfig {
- base_ms: 1,
- max_ms: 2,
- factor: 1,
- jitter_ms: 0,
- },
- )
- .await;
- tokio::time::sleep(std::time::Duration::from_millis(20)).await;
- handle.stop();
- handle.stopped().await;
- }
-
- #[test]
- fn runtime_startup_report_prefers_explicit_cli_paths() {
- let args = cli_args {
- service: rhi::host_runtime::ServiceCliArgs {
- config: Some(PathBuf::from("/tmp/rhi/config.toml")),
- identity: Some(PathBuf::from("/tmp/rhi/identity.secret.json")),
- allow_generate_identity: false,
- },
- command: None,
- };
- let mut settings = minimal_settings();
- settings.config.service.logs_dir = "/tmp/rhi/logs".to_string();
- settings.config.logging.output_dir = PathBuf::from("/tmp/rhi/logs");
- settings.config.subscriber.state.path = PathBuf::from("/tmp/rhi/state.json");
-
- let contract = sample_runtime_contract();
- let report = runtime_startup_report(&args, &settings, &contract);
-
- assert_eq!(
- report,
- RhiRuntimeStartupReport {
- active_profile: "interactive_user".to_string(),
- config_path: PathBuf::from("/tmp/rhi/config.toml"),
- config_path_source: "cli_arg".to_string(),
- canonical_config_path: PathBuf::from(
- "/home/treesap/.radroots/config/workers/rhi/config.toml"
- ),
- logs_dir: PathBuf::from("/tmp/rhi/logs"),
- logs_dir_source: "config_artifact".to_string(),
- canonical_logs_dir: PathBuf::from("/home/treesap/.radroots/logs/workers/rhi"),
- identity_path: PathBuf::from("/tmp/rhi/identity.secret.json"),
- identity_path_source: "cli_arg".to_string(),
- canonical_identity_path: PathBuf::from(
- "/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json"
- ),
- subscriber_state_path: PathBuf::from("/tmp/rhi/state.json"),
- subscriber_state_path_source: "config_artifact".to_string(),
- canonical_subscriber_state_path: PathBuf::from(
- "/home/treesap/.radroots/data/workers/rhi/trade-agreement-attestation/state.json"
- ),
- path_overrides: sample_runtime_contract().path_overrides,
- default_shared_secret_backend: "encrypted_file".to_string(),
- allowed_shared_secret_backends: vec!["encrypted_file".to_string()],
- }
- );
- }
-
- #[test]
- fn runtime_startup_report_falls_back_to_canonical_contract_paths() {
- let args = cli_args {
- command: None,
- service: rhi::host_runtime::ServiceCliArgs {
- config: None,
- identity: None,
- allow_generate_identity: false,
- },
- };
- let contract = sample_runtime_contract();
- let mut settings = minimal_settings();
- settings.config.service.logs_dir = contract.canonical_logs_dir.display().to_string();
- settings.config.logging.output_dir = contract.canonical_logs_dir.clone();
- settings.config.subscriber.state.path = contract.canonical_subscriber_state_path.clone();
-
- let report = runtime_startup_report(&args, &settings, &contract);
-
- assert_eq!(report.config_path, contract.canonical_config_path);
- assert_eq!(report.config_path_source, "profile_default");
- assert_eq!(report.logs_dir, contract.canonical_logs_dir);
- assert_eq!(report.logs_dir_source, "profile_default");
- assert_eq!(report.identity_path, contract.canonical_identity_path);
- assert_eq!(report.identity_path_source, "profile_default");
- assert_eq!(
- report.subscriber_state_path,
- contract.canonical_subscriber_state_path
- );
- assert_eq!(report.subscriber_state_path_source, "profile_default");
- assert_eq!(report.path_overrides, contract.path_overrides);
- assert_eq!(report.default_shared_secret_backend, "encrypted_file");
- assert_eq!(
- report.allowed_shared_secret_backends,
- vec!["encrypted_file".to_string()]
- );
- }
}
diff --git a/src/paths.rs b/src/paths.rs
@@ -1,227 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use crate::host_paths::{
- RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace,
-};
-use anyhow::{Context, Result, bail};
-use serde::Serialize;
-
-const RHI_RUNTIME_ID: &str = "rhi";
-const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml";
-const DEFAULT_SERVICE_IDENTITY_FILE_NAME: &str = "identity.secret.json";
-const SUBSCRIBER_STATE_DIR_NAME: &str = "trade-agreement-attestation";
-const SUBSCRIBER_STATE_FILE_NAME: &str = "state.json";
-const RHI_PATHS_PROFILE_ENV: &str = "RHI_PATHS_PROFILE";
-const RHI_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RHI_PATHS_REPO_LOCAL_ROOT";
-const RHI_DEFAULT_SHARED_SECRET_BACKEND: &str = "encrypted_file";
-const RHI_ALLOWED_PROFILES: [&str; 3] = ["interactive_user", "service_host", "repo_local"];
-const RHI_ALLOWED_SHARED_SECRET_BACKENDS: [&str; 1] = ["encrypted_file"];
-const SUBORDINATE_PATH_OVERRIDE_SOURCE: &str = "config_artifact";
-const SUBORDINATE_PATH_OVERRIDE_KEYS: [&str; 2] = ["logging.output_dir", "subscriber.state.path"];
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct RhiRuntimePaths {
- pub(crate) config_path: PathBuf,
- pub(crate) logs_dir: PathBuf,
- pub(crate) identity_path: PathBuf,
- pub(crate) subscriber_state_path: PathBuf,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct RhiRuntimeContractOutput {
- pub active_profile: String,
- pub allowed_profiles: Vec<String>,
- pub path_overrides: RhiRuntimePathOverrideContractOutput,
- pub default_shared_secret_backend: String,
- pub allowed_shared_secret_backends: Vec<String>,
- pub canonical_config_path: PathBuf,
- pub canonical_logs_dir: PathBuf,
- pub canonical_identity_path: PathBuf,
- pub canonical_subscriber_state_path: PathBuf,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct RhiRuntimePathOverrideContractOutput {
- pub profile_source: String,
- pub root_source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub repo_local_root: Option<PathBuf>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub repo_local_root_source: Option<String>,
- pub subordinate_path_override_source: String,
- pub subordinate_path_override_keys: Vec<String>,
-}
-
-struct RhiRuntimePathSelection {
- profile: RadrootsPathProfile,
- profile_source: String,
- repo_local_root: Option<PathBuf>,
- repo_local_root_source: Option<String>,
-}
-
-fn parse_path_profile(value: &str) -> Result<RadrootsPathProfile> {
- match value {
- "interactive_user" => Ok(RadrootsPathProfile::InteractiveUser),
- "service_host" => Ok(RadrootsPathProfile::ServiceHost),
- "repo_local" => Ok(RadrootsPathProfile::RepoLocal),
- _ => bail!(
- "{RHI_PATHS_PROFILE_ENV} must be `interactive_user`, `service_host`, or `repo_local`"
- ),
- }
-}
-
-pub(crate) fn process_path_selection() -> Result<(RadrootsPathProfile, Option<PathBuf>)> {
- let selection = process_path_selection_with_sources()?;
- Ok((selection.profile, selection.repo_local_root))
-}
-
-fn process_path_selection_with_sources() -> Result<RhiRuntimePathSelection> {
- let profile = match std::env::var(RHI_PATHS_PROFILE_ENV) {
- Ok(value) => (
- parse_path_profile(&value)?,
- format!("process_env:{RHI_PATHS_PROFILE_ENV}"),
- ),
- Err(std::env::VarError::NotPresent) => {
- (RadrootsPathProfile::InteractiveUser, "default".to_owned())
- }
- Err(std::env::VarError::NotUnicode(_)) => {
- bail!("{RHI_PATHS_PROFILE_ENV} must be valid utf-8 when set")
- }
- };
- let repo_local_root_raw = std::env::var_os(RHI_PATHS_REPO_LOCAL_ROOT_ENV);
- let repo_local_root = repo_local_root_raw.as_ref().map(PathBuf::from);
- Ok(RhiRuntimePathSelection {
- profile: profile.0,
- profile_source: profile.1,
- repo_local_root,
- repo_local_root_source: repo_local_root_raw
- .as_ref()
- .map(|_| format!("process_env:{RHI_PATHS_REPO_LOCAL_ROOT_ENV}")),
- })
-}
-
-fn path_overrides_for(
- profile: RadrootsPathProfile,
- repo_local_root: Option<&Path>,
-) -> Result<RadrootsPathOverrides> {
- match profile {
- RadrootsPathProfile::RepoLocal => {
- let repo_local_root = repo_local_root.context(format!(
- "{RHI_PATHS_REPO_LOCAL_ROOT_ENV} must be set when {RHI_PATHS_PROFILE_ENV}=repo_local"
- ))?;
- Ok(RadrootsPathOverrides::repo_local(repo_local_root))
- }
- _ => Ok(RadrootsPathOverrides::default()),
- }
-}
-
-pub(crate) fn resolve_runtime_paths_with_resolver(
- resolver: &RadrootsPathResolver,
- profile: RadrootsPathProfile,
- repo_local_root: Option<&Path>,
-) -> Result<RhiRuntimePaths> {
- let namespace = RadrootsRuntimeNamespace::worker(RHI_RUNTIME_ID)
- .map_err(|error| anyhow::anyhow!("resolve rhi namespace: {error}"))?;
- let overrides = path_overrides_for(profile, repo_local_root)?;
- let namespaced = resolver
- .resolve(profile, &overrides)
- .map_err(|error| anyhow::anyhow!("resolve rhi runtime paths: {error}"))?
- .namespaced(&namespace);
- Ok(RhiRuntimePaths {
- config_path: namespaced.config.join(DEFAULT_CONFIG_FILE_NAME),
- logs_dir: namespaced.logs,
- identity_path: namespaced.secrets.join(DEFAULT_SERVICE_IDENTITY_FILE_NAME),
- subscriber_state_path: namespaced
- .data
- .join(SUBSCRIBER_STATE_DIR_NAME)
- .join(SUBSCRIBER_STATE_FILE_NAME),
- })
-}
-
-pub(crate) fn default_runtime_paths_for_process() -> Result<RhiRuntimePaths> {
- let (profile, repo_local_root) = process_path_selection()?;
- resolve_runtime_paths_with_resolver(
- &RadrootsPathResolver::current(),
- profile,
- repo_local_root.as_deref(),
- )
-}
-
-pub fn default_config_path_for_process() -> Result<PathBuf> {
- Ok(default_runtime_paths_for_process()?.config_path)
-}
-
-pub fn default_identity_path_for_process() -> Result<PathBuf> {
- Ok(default_runtime_paths_for_process()?.identity_path)
-}
-
-pub fn default_subscriber_state_path_for_process() -> Result<PathBuf> {
- Ok(default_runtime_paths_for_process()?.subscriber_state_path)
-}
-
-pub fn runtime_contract_for_process() -> Result<RhiRuntimeContractOutput> {
- let selection = process_path_selection_with_sources()?;
- runtime_contract_with_selection(&RadrootsPathResolver::current(), &selection)
-}
-
-#[cfg(test)]
-pub(crate) fn runtime_contract_with_resolver(
- resolver: &RadrootsPathResolver,
- profile: RadrootsPathProfile,
- repo_local_root: Option<&Path>,
-) -> Result<RhiRuntimeContractOutput> {
- runtime_contract_with_selection(
- resolver,
- &RhiRuntimePathSelection {
- profile,
- profile_source: "caller".to_owned(),
- repo_local_root: repo_local_root.map(Path::to_path_buf),
- repo_local_root_source: repo_local_root.map(|_| "caller".to_owned()),
- },
- )
-}
-
-fn runtime_contract_with_selection(
- resolver: &RadrootsPathResolver,
- selection: &RhiRuntimePathSelection,
-) -> Result<RhiRuntimeContractOutput> {
- let profile = selection.profile;
- let repo_local_root = selection.repo_local_root.as_deref();
- let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?;
- Ok(RhiRuntimeContractOutput {
- active_profile: profile.to_string(),
- allowed_profiles: RHI_ALLOWED_PROFILES
- .into_iter()
- .map(str::to_owned)
- .collect(),
- path_overrides: RhiRuntimePathOverrideContractOutput {
- profile_source: selection.profile_source.clone(),
- root_source: root_source_for_profile(profile).to_owned(),
- repo_local_root: selection.repo_local_root.clone(),
- repo_local_root_source: selection.repo_local_root_source.clone(),
- subordinate_path_override_source: SUBORDINATE_PATH_OVERRIDE_SOURCE.to_owned(),
- subordinate_path_override_keys: SUBORDINATE_PATH_OVERRIDE_KEYS
- .into_iter()
- .map(str::to_owned)
- .collect(),
- },
- default_shared_secret_backend: RHI_DEFAULT_SHARED_SECRET_BACKEND.to_owned(),
- allowed_shared_secret_backends: RHI_ALLOWED_SHARED_SECRET_BACKENDS
- .into_iter()
- .map(str::to_owned)
- .collect(),
- canonical_config_path: paths.config_path,
- canonical_logs_dir: paths.logs_dir,
- canonical_identity_path: paths.identity_path,
- canonical_subscriber_state_path: paths.subscriber_state_path,
- })
-}
-
-fn root_source_for_profile(profile: RadrootsPathProfile) -> &'static str {
- match profile {
- RadrootsPathProfile::InteractiveUser => "host_defaults",
- RadrootsPathProfile::ServiceHost => "service_host_defaults",
- RadrootsPathProfile::RepoLocal => "repo_local_root",
- RadrootsPathProfile::MobileNative => "mobile_native_defaults",
- }
-}
diff --git a/src/runtime_context.rs b/src/runtime_context.rs
@@ -0,0 +1,183 @@
+//! Sealed bootstrap binding for one canonical RHI service instance.
+
+use core::fmt;
+use std::{error::Error, path::Path};
+
+use radroots_runtime_paths::{
+ RadrootsPathProfile, RadrootsPathResolver, RadrootsServiceInstanceArtifacts, RuntimeContext,
+ RuntimeContextBootstrap, RuntimeContextSource, ServiceCredentialArtifactName, ServiceId,
+ default_service_instance_artifacts, service_credential_artifact_path,
+};
+
+use crate::{RhiBootstrapProfileV1, RhiCliInvocationV1};
+
+const RHI_SERVICE_ID: &str = "rhi";
+const RHI_IDENTITY_ARTIFACT_NAME: &str = "service.identity.ncrypt";
+
+/// Stable source-free classification for RHI runtime-context failures.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiRuntimeContextErrorKind {
+ InvalidServiceIdentity,
+ InvalidBootstrapBinding,
+ PathSelection,
+}
+
+impl RhiRuntimeContextErrorKind {
+ const fn message(self) -> &'static str {
+ match self {
+ Self::InvalidServiceIdentity => "RHI service identity is invalid",
+ Self::InvalidBootstrapBinding => "RHI bootstrap selectors are inconsistent",
+ Self::PathSelection => "RHI runtime path selection failed",
+ }
+ }
+}
+
+/// One redacted RHI runtime-context failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiRuntimeContextError {
+ kind: RhiRuntimeContextErrorKind,
+}
+
+impl RhiRuntimeContextError {
+ const fn new(kind: RhiRuntimeContextErrorKind) -> Self {
+ Self { kind }
+ }
+
+ /// Returns the stable failure classification.
+ #[must_use]
+ pub const fn kind(self) -> RhiRuntimeContextErrorKind {
+ self.kind
+ }
+}
+
+impl fmt::Debug for RhiRuntimeContextError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiRuntimeContextError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl fmt::Display for RhiRuntimeContextError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(self.kind.message())
+ }
+}
+
+impl Error for RhiRuntimeContextError {}
+
+/// Immutable canonical paths and bootstrap selection for one RHI instance.
+///
+/// Construction is sealed to the validated CLI invocation and the shared
+/// runtime-path resolver. Callers cannot forge another service identity, path
+/// set, artifact name, or selected configuration path:
+///
+/// ```compile_fail
+/// use rhi::RhiRuntimeContext;
+///
+/// let _ = RhiRuntimeContext {
+/// context: todo!(),
+/// artifacts: todo!(),
+/// selected_config_path: todo!(),
+/// profile: todo!(),
+/// };
+/// ```
+#[derive(Clone, PartialEq, Eq)]
+pub struct RhiRuntimeContext {
+ context: RuntimeContext,
+ artifacts: RadrootsServiceInstanceArtifacts,
+ identity_path: std::path::PathBuf,
+ selected_config_path: std::path::PathBuf,
+ profile: RhiBootstrapProfileV1,
+}
+
+impl RhiRuntimeContext {
+ /// Returns the shared immutable service-instance context.
+ #[must_use]
+ pub const fn context(&self) -> &RuntimeContext {
+ &self.context
+ }
+
+ /// Returns the exact common service artifacts.
+ #[must_use]
+ pub const fn artifacts(&self) -> &RadrootsServiceInstanceArtifacts {
+ &self.artifacts
+ }
+
+ /// Returns the exact validated encrypted service-identity artifact path.
+ #[must_use]
+ pub fn identity_path(&self) -> &Path {
+ &self.identity_path
+ }
+
+ /// Returns the explicit or canonical configuration artifact selected once.
+ #[must_use]
+ pub fn selected_config_path(&self) -> &Path {
+ &self.selected_config_path
+ }
+
+ /// Returns the validated bootstrap profile.
+ #[must_use]
+ pub const fn profile(&self) -> RhiBootstrapProfileV1 {
+ self.profile
+ }
+}
+
+impl fmt::Debug for RhiRuntimeContext {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiRuntimeContext")
+ .field("profile", &self.profile)
+ .field("service", &RHI_SERVICE_ID)
+ .field("instance", &"[redacted]")
+ .field("paths", &"[redacted]")
+ .finish()
+ }
+}
+
+/// Resolves one validated CLI selection into the sole RHI path authority.
+pub fn resolve_rhi_runtime_context(
+ resolver: &RadrootsPathResolver,
+ invocation: &RhiCliInvocationV1,
+) -> Result<RhiRuntimeContext, RhiRuntimeContextError> {
+ let profile = invocation.profile();
+ let path_profile = match profile {
+ RhiBootstrapProfileV1::ServiceHost => RadrootsPathProfile::ServiceHost,
+ RhiBootstrapProfileV1::Interactive => RadrootsPathProfile::InteractiveUser,
+ RhiBootstrapProfileV1::RepoLocal => RadrootsPathProfile::RepoLocal,
+ };
+ let bootstrap = RuntimeContextBootstrap::new(
+ path_profile,
+ invocation.repo_local_root().map(Path::to_path_buf),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .map_err(|_| {
+ RhiRuntimeContextError::new(RhiRuntimeContextErrorKind::InvalidBootstrapBinding)
+ })?;
+ let service = ServiceId::new(RHI_SERVICE_ID).map_err(|_| {
+ RhiRuntimeContextError::new(RhiRuntimeContextErrorKind::InvalidServiceIdentity)
+ })?;
+ let context =
+ RuntimeContext::resolve(resolver, bootstrap, service, invocation.instance().clone())
+ .map_err(|_| RhiRuntimeContextError::new(RhiRuntimeContextErrorKind::PathSelection))?;
+ let artifacts = default_service_instance_artifacts(context.paths());
+ let identity_name =
+ ServiceCredentialArtifactName::new(RHI_IDENTITY_ARTIFACT_NAME).map_err(|_| {
+ RhiRuntimeContextError::new(RhiRuntimeContextErrorKind::InvalidServiceIdentity)
+ })?;
+ let identity_path = service_credential_artifact_path(context.paths(), &identity_name);
+ let selected_config_path = invocation
+ .config_path()
+ .map(Path::to_path_buf)
+ .unwrap_or_else(|| artifacts.config().to_path_buf());
+
+ Ok(RhiRuntimeContext {
+ context,
+ artifacts,
+ identity_path,
+ selected_config_path,
+ profile,
+ })
+}
diff --git a/tests/services_hardening_cli.rs b/tests/services_hardening_cli.rs
@@ -4,7 +4,7 @@ use std::error::Error;
use std::path::Path;
use rhi::{
- RHI_INSTANCE_ID_MAX_BYTES, RhiBootstrapProfileV1, RhiCliOutputModeV1, RhiCliV1ErrorKind,
+ INSTANCE_ID_MAX_BYTES, RhiBootstrapProfileV1, RhiCliOutputModeV1, RhiCliV1ErrorKind,
RhiCommandV1, RhiConfigCommandV1, RhiIdentityCommandV1, RhiMetricsCommandV1,
RhiPresenceCommandV1, RhiPublicationCommandV1, RhiReconciliationCommandV1, RhiSourcesCommandV1,
RhiStateCommandV1, RhiTradeCommandV1, parse_rhi_cli_v1_from,
@@ -158,12 +158,12 @@ fn bootstrap_values_are_explicit_bounded_and_cross_bound() {
])
.expect("repo-local invocation");
assert_eq!(repo.profile(), RhiBootstrapProfileV1::RepoLocal);
- assert_eq!(repo.instance(), "review_01");
+ assert_eq!(repo.instance().as_str(), "review_01");
assert_eq!(repo.repo_local_root(), Some(Path::new("/repo/radroots")));
assert_eq!(repo.config_path(), Some(Path::new("/repo/config/rhi.toml")));
assert_eq!(repo.output_mode(), RhiCliOutputModeV1::Json);
- let exact = "a".repeat(RHI_INSTANCE_ID_MAX_BYTES);
+ let exact = "a".repeat(INSTANCE_ID_MAX_BYTES);
assert!(
parse_rhi_cli_v1_from([
"rhi",
@@ -175,7 +175,7 @@ fn bootstrap_values_are_explicit_bounded_and_cross_bound() {
])
.is_ok()
);
- let over = "a".repeat(RHI_INSTANCE_ID_MAX_BYTES + 1);
+ let over = "a".repeat(INSTANCE_ID_MAX_BYTES + 1);
assert_eq!(
parse_rhi_cli_v1_from([
"rhi",
diff --git a/tests/services_hardening_runtime_context.rs b/tests/services_hardening_runtime_context.rs
@@ -0,0 +1,290 @@
+#![forbid(unsafe_code)]
+
+use std::error::Error;
+use std::path::{Path, PathBuf};
+
+use rhi::{
+ RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform,
+ RhiBootstrapProfileV1, RuntimeContextSource, parse_rhi_cli_v1_from,
+ resolve_rhi_runtime_context,
+};
+
+fn resolve(
+ resolver: &RadrootsPathResolver,
+ profile: &str,
+ instance: &str,
+ repo_local_root: Option<&str>,
+ config_path: Option<&str>,
+) -> rhi::RhiRuntimeContext {
+ let mut arguments = vec!["rhi", "--profile", profile, "--instance", instance];
+ if let Some(root) = repo_local_root {
+ arguments.extend(["--repo-local-root", root]);
+ }
+ if let Some(path) = config_path {
+ arguments.extend(["--config", path]);
+ }
+ arguments.push("run");
+ let invocation = parse_rhi_cli_v1_from(arguments).expect("validated invocation");
+ resolve_rhi_runtime_context(resolver, &invocation).expect("runtime context")
+}
+
+fn assert_roots(context: &rhi::RhiRuntimeContext, expected: [&str; 6]) {
+ let paths = context.context().paths();
+ assert_eq!(paths.config(), Path::new(expected[0]));
+ assert_eq!(paths.state(), Path::new(expected[1]));
+ assert_eq!(paths.cache(), Path::new(expected[2]));
+ assert_eq!(paths.logs(), Path::new(expected[3]));
+ assert_eq!(paths.run(), Path::new(expected[4]));
+ assert_eq!(paths.secrets(), Path::new(expected[5]));
+}
+
+#[test]
+fn repo_local_context_binds_identity_provenance_and_exact_artifacts() {
+ let resolver =
+ RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
+ let primary = resolve(
+ &resolver,
+ "repo-local",
+ "primary",
+ Some("/repo/.local/radroots"),
+ None,
+ );
+ let secondary = resolve(
+ &resolver,
+ "repo-local",
+ "secondary",
+ Some("/repo/.local/radroots"),
+ None,
+ );
+
+ assert_eq!(primary.context().service().as_str(), "rhi");
+ assert_eq!(primary.context().instance().as_str(), "primary");
+ assert_eq!(primary.profile(), RhiBootstrapProfileV1::RepoLocal);
+ assert_eq!(primary.context().profile(), RadrootsPathProfile::RepoLocal);
+ assert_eq!(
+ primary.context().sources().service(),
+ RuntimeContextSource::SafeDefault
+ );
+ assert_eq!(
+ primary.context().sources().instance(),
+ RuntimeContextSource::BootstrapCli
+ );
+ assert_eq!(
+ primary.context().sources().profile(),
+ RuntimeContextSource::BootstrapCli
+ );
+ assert_eq!(
+ primary.context().sources().repo_local_root(),
+ Some(RuntimeContextSource::BootstrapCli)
+ );
+ assert_eq!(
+ primary.context().sources().paths(),
+ RuntimeContextSource::DerivedPath
+ );
+ assert_roots(
+ &primary,
+ [
+ "/repo/.local/radroots/config/services/rhi/primary",
+ "/repo/.local/radroots/data/services/rhi/primary",
+ "/repo/.local/radroots/cache/services/rhi/primary",
+ "/repo/.local/radroots/logs/services/rhi/primary",
+ "/repo/.local/radroots/run/services/rhi/primary",
+ "/repo/.local/radroots/secrets/services/rhi/primary",
+ ],
+ );
+ assert_eq!(
+ primary.artifacts().config(),
+ Path::new("/repo/.local/radroots/config/services/rhi/primary/config.toml")
+ );
+ assert_eq!(
+ primary.artifacts().state_database(),
+ Path::new("/repo/.local/radroots/data/services/rhi/primary/state.sqlite")
+ );
+ assert_eq!(
+ primary.artifacts().state_lock(),
+ Path::new("/repo/.local/radroots/data/services/rhi/primary/state.lock")
+ );
+ assert_eq!(
+ primary.artifacts().admin_socket(),
+ Path::new("/repo/.local/radroots/run/services/rhi/primary/admin.sock")
+ );
+ assert_eq!(
+ primary.identity_path(),
+ Path::new("/repo/.local/radroots/secrets/services/rhi/primary/service.identity.ncrypt")
+ );
+ assert_eq!(primary.selected_config_path(), primary.artifacts().config());
+ assert_ne!(primary.context().paths(), secondary.context().paths());
+}
+
+#[test]
+fn service_host_and_interactive_profiles_have_exact_roots() {
+ let service_host = resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ "service-host",
+ "default",
+ None,
+ None,
+ );
+ assert_roots(
+ &service_host,
+ [
+ "/etc/radroots/services/rhi/default",
+ "/var/lib/radroots/services/rhi/default",
+ "/var/cache/radroots/services/rhi/default",
+ "/var/log/radroots/services/rhi/default",
+ "/run/radroots/services/rhi/default",
+ "/etc/radroots/secrets/services/rhi/default",
+ ],
+ );
+
+ let interactive = resolve(
+ &RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/operator")),
+ xdg_config_home: Some(PathBuf::from("/xdg/config")),
+ xdg_data_home: Some(PathBuf::from("/xdg/data")),
+ xdg_state_home: Some(PathBuf::from("/xdg/state")),
+ xdg_cache_home: Some(PathBuf::from("/xdg/cache")),
+ xdg_runtime_dir: Some(PathBuf::from("/xdg/run")),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
+ "interactive",
+ "default",
+ None,
+ Some("/operator/rhi.toml"),
+ );
+ assert_roots(
+ &interactive,
+ [
+ "/xdg/config/radroots/services/rhi/default",
+ "/xdg/data/radroots/services/rhi/default",
+ "/xdg/cache/radroots/services/rhi/default",
+ "/xdg/state/radroots/logs/services/rhi/default",
+ "/xdg/run/radroots/services/rhi/default",
+ "/xdg/config/radroots/secrets/services/rhi/default",
+ ],
+ );
+ assert_eq!(
+ interactive.selected_config_path(),
+ Path::new("/operator/rhi.toml")
+ );
+}
+
+#[test]
+fn macos_and_windows_interactive_roots_remain_exact_and_injected() {
+ let macos = resolve(
+ &RadrootsPathResolver::new(
+ RadrootsPlatform::Macos,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/Users/operator")),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
+ "interactive",
+ "default",
+ None,
+ None,
+ );
+ assert_roots(
+ &macos,
+ [
+ "/Users/operator/Library/Application Support/Radroots/config/services/rhi/default",
+ "/Users/operator/Library/Application Support/Radroots/data/services/rhi/default",
+ "/Users/operator/Library/Caches/Radroots/services/rhi/default",
+ "/Users/operator/Library/Logs/Radroots/services/rhi/default",
+ "/Users/operator/Library/Application Support/Radroots/run/services/rhi/default",
+ "/Users/operator/Library/Application Support/Radroots/secrets/services/rhi/default",
+ ],
+ );
+
+ let windows = resolve(
+ &RadrootsPathResolver::new(
+ RadrootsPlatform::Windows,
+ RadrootsHostEnvironment {
+ appdata_dir: Some(PathBuf::from(r"C:\Users\operator\AppData\Roaming")),
+ localappdata_dir: Some(PathBuf::from(r"C:\Users\operator\AppData\Local")),
+ ..RadrootsHostEnvironment::default()
+ },
+ ),
+ "interactive",
+ "default",
+ None,
+ None,
+ );
+ assert_roots(
+ &windows,
+ [
+ r"C:\Users\operator\AppData\Roaming/Radroots/config/services/rhi/default",
+ r"C:\Users\operator\AppData\Local/Radroots/data/services/rhi/default",
+ r"C:\Users\operator\AppData\Local/Radroots/cache/services/rhi/default",
+ r"C:\Users\operator\AppData\Local/Radroots/logs/services/rhi/default",
+ r"C:\Users\operator\AppData\Local/Radroots/run/services/rhi/default",
+ r"C:\Users\operator\AppData\Roaming/Radroots/secrets/services/rhi/default",
+ ],
+ );
+}
+
+#[test]
+fn debug_and_errors_do_not_disclose_paths_or_instances() {
+ let context = resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ "repo-local",
+ "secret-instance",
+ Some("/secret/root"),
+ Some("/secret/config.toml"),
+ );
+ let rendered = format!("{context:?}");
+ for forbidden in ["secret-instance", "/secret/root", "/secret/config.toml"] {
+ assert!(!rendered.contains(forbidden), "{rendered}");
+ }
+
+ let invocation = parse_rhi_cli_v1_from([
+ "rhi",
+ "--profile",
+ "interactive",
+ "--instance",
+ "default",
+ "run",
+ ])
+ .expect("invocation");
+ let error = resolve_rhi_runtime_context(
+ &RadrootsPathResolver::new(RadrootsPlatform::Macos, RadrootsHostEnvironment::default()),
+ &invocation,
+ )
+ .expect_err("missing HOME");
+ assert!(error.source().is_none());
+ assert!(!format!("{error:?} {error}").contains("HOME"));
+}
+
+#[test]
+fn source_contains_no_legacy_path_authority() {
+ let lib = include_str!("../src/lib.rs");
+ let context = include_str!("../src/runtime_context.rs");
+ let main = include_str!("../src/main.rs");
+ let config = include_str!("../src/config.rs");
+ for forbidden in [
+ "pub mod host_paths",
+ "pub mod paths",
+ "RHI_PATHS_PROFILE",
+ "RHI_PATHS_REPO_LOCAL_ROOT",
+ "RadrootsRuntimeNamespace::worker",
+ "RhiRuntimeStartupReport",
+ "RhiRuntimeContractOutput",
+ "default_config_path_for_process",
+ "default_identity_path_for_process",
+ "default_subscriber_state_path_for_process",
+ ] {
+ assert!(!lib.contains(forbidden));
+ assert!(!context.contains(forbidden));
+ assert!(!main.contains(forbidden));
+ assert!(!config.contains(forbidden));
+ }
+ for removed in ["src/paths.rs", "src/host_paths/mod.rs", "src/cli.rs"] {
+ assert!(
+ !Path::new(env!("CARGO_MANIFEST_DIR")).join(removed).exists(),
+ "legacy path authority remains at {removed}"
+ );
+ }
+}
diff --git a/tests/source_guards.rs b/tests/source_guards.rs
@@ -94,7 +94,7 @@ fn rhi_release_product_surface_has_no_order_or_receipt_modules() {
fn rhi_agreement_attestation_is_release_product_optional_infrastructure() {
let worker = read_repo_file("src/features/trade_agreement_attestation.rs");
let lib = read_repo_file("src/lib.rs");
- let cli = read_repo_file("src/cli.rs");
+ let cli = read_repo_file("src/cli_v1.rs");
let config = read_repo_file("src/config.rs");
for required in [
@@ -132,10 +132,10 @@ fn rhi_agreement_attestation_is_release_product_optional_infrastructure() {
"RHI service presence must advertise canonical release-product trade mutation kinds"
);
assert!(
- cli.contains("attestation-smoke")
- && !cli.contains("proof-smoke")
+ !cli.contains("AttestationSmoke")
+ && !cli.contains("ProofSmoke")
&& !cli.contains("remote-prove"),
- "RHI CLI must expose only release-product agreement attestation smoke command"
+ "RHI CLI must not retain prototype smoke commands"
);
assert!(
config.contains("settings.config.trade_agreement_attestation.validate()?"),
@@ -145,20 +145,24 @@ fn rhi_agreement_attestation_is_release_product_optional_infrastructure() {
#[test]
fn rhi_state_paths_are_named_for_agreement_attestation() {
- let paths = read_repo_file("src/paths.rs");
let config = read_repo_file("src/config.rs");
- let main = read_repo_file("src/main.rs");
+ let context = read_repo_file("src/runtime_context.rs");
- for source in [paths.as_str(), config.as_str(), main.as_str()] {
- assert!(
- source.contains("trade-agreement-attestation"),
- "RHI runtime state paths must use agreement-attestation naming"
- );
+ assert!(
+ config.contains("trade-agreement-attestation"),
+ "transitional RHI state paths must use agreement-attestation naming"
+ );
+ for source in [config.as_str(), context.as_str()] {
assert!(
!source.contains("trade-listing"),
"RHI runtime state paths must not retain trade-listing naming"
);
}
+ assert!(
+ context.contains("default_service_instance_artifacts")
+ && context.contains("service.identity.ncrypt"),
+ "RHI path authority must derive exact common and credential artifacts"
+ );
}
fn read_repo_file(relative_path: &str) -> String {