commit 31be69136f0517c2346f11453a6b2906d31d266b
parent 490f4ba67a35c8ff6ea4f0b5b25bc704e5f8c808
Author: triesap <tyson@radroots.org>
Date: Thu, 4 Jun 2026 14:48:27 -0700
config: harden runtime config names
- switch runtime env and env-file keys to RADROOTS_CLI names
- make CLI TOML strict with relays and signer backend schema
- document canonical config and invalid previous names
- verify nix run .#fmt, nix run .#check, nix run .#test, and git diff --check
Diffstat:
9 files changed, 663 insertions(+), 250 deletions(-)
diff --git a/.env.example b/.env.example
@@ -1,4 +1,9 @@
-# copy to .env for local cli development; the cli loads .env by default from the repo root
+# Copy to .env for local CLI development; the CLI loads .env by default from the repo root.
+RADROOTS_CLI_PATHS_PROFILE=repo_local
+RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT=infra/local/runtime/radroots
+RADROOTS_CLI_OUTPUT_FORMAT=human
RADROOTS_CLI_LOGGING_FILTER=info
-RADROOTS_CLI_LOGGING_OUTPUT_DIR=/absolute/path/to/radroots-platform-v1/logs/services/local/radroots-cli
+RADROOTS_CLI_LOGGING_OUTPUT_DIR=infra/local/runtime/radroots/logs/apps/cli
RADROOTS_CLI_LOGGING_STDOUT=false
+RADROOTS_CLI_ACCOUNT_SECRET_BACKEND=encrypted_file
+RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK=none
diff --git a/Cargo.lock b/Cargo.lock
@@ -3535,6 +3535,7 @@ dependencies = [
"radroots_replica_db",
"radroots_replica_db_schema",
"radroots_replica_sync",
+ "radroots_runtime",
"radroots_runtime_paths",
"radroots_sdk",
"radroots_secret_vault",
diff --git a/Cargo.toml b/Cargo.toml
@@ -39,6 +39,7 @@ radroots_protected_store = { path = "../lib/crates/protected_store", features =
radroots_replica_db = { path = "../lib/crates/replica_db" }
radroots_replica_db_schema = { path = "../lib/crates/replica_db_schema" }
radroots_replica_sync = { path = "../lib/crates/replica_sync" }
+radroots_runtime = { path = "../lib/crates/runtime" }
radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
radroots_sdk = { path = "../lib/crates/sdk", features = ["radrootsd-client", "relay-client", "signing"] }
radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] }
diff --git a/src/out/envelope.rs b/src/out/envelope.rs
@@ -281,13 +281,13 @@ fn next_actions_from_actions_value(actions_value: Option<&Value>) -> Vec<NextAct
fn next_action_from_action_string(action: &str) -> Option<NextAction> {
let action = action.trim();
- if action == "configure RADROOTS_RPC_BEARER_TOKEN" {
+ if action == "configure RADROOTS_CLI_RPC_BEARER_TOKEN" {
return Some(NextAction {
kind: NextActionKind::OperatorConfig,
label: "configure rpc bearer token".to_owned(),
command: None,
description: Some(action.to_owned()),
- env_var: Some("RADROOTS_RPC_BEARER_TOKEN".to_owned()),
+ env_var: Some("RADROOTS_CLI_RPC_BEARER_TOKEN".to_owned()),
config_key: None,
});
}
@@ -611,9 +611,9 @@ mod tests {
);
error.detail = Some(json!({
"actions": [
- "configure RADROOTS_RPC_BEARER_TOKEN",
+ "configure RADROOTS_CLI_RPC_BEARER_TOKEN",
"configure signer.remote_nip46 signer_session_ref",
- "configure RADROOTS_RPC_BEARER_TOKEN"
+ "configure RADROOTS_CLI_RPC_BEARER_TOKEN"
]
}));
let envelope = OutputEnvelope::failure(
@@ -632,7 +632,7 @@ mod tests {
assert_eq!(envelope.next_actions[0].command, None);
assert_eq!(
envelope.next_actions[0].env_var.as_deref(),
- Some("RADROOTS_RPC_BEARER_TOKEN")
+ Some("RADROOTS_CLI_RPC_BEARER_TOKEN")
);
assert_eq!(
envelope.next_actions[1].kind,
diff --git a/src/runtime/account.rs b/src/runtime/account.rs
@@ -17,7 +17,7 @@ use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::view::runtime::{AccountResolutionView, AccountSummaryView};
-const HOST_VAULT_AVAILABILITY_OVERRIDE_ENV: &str = "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE";
+const HOST_VAULT_AVAILABILITY_OVERRIDE_ENV: &str = "RADROOTS_CLI_ACCOUNT_HOST_VAULT_AVAILABLE";
const HOST_VAULT_SERVICE_NAME: &str = "org.radroots.cli.local-account";
const HOST_VAULT_PROBE_SLOT: &str = "__radroots_cli_host_vault_probe__";
pub const SHARED_ACCOUNT_STORE_SOURCE: &str = "shared account store ยท local first";
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -5,6 +5,7 @@ use std::path::Path;
use std::path::PathBuf;
use radroots_local_events::{RelayUrlValidationError, normalize_relay_url};
+use radroots_runtime::{parse_bool_value, parse_strict_env_file, parse_u64_value};
use radroots_runtime_paths::{
RadrootsLegacyPathCandidate, RadrootsMigrationReport, RadrootsPathResolver,
inspect_legacy_paths,
@@ -33,52 +34,46 @@ const CLI_DEFAULT_SECRET_BACKEND: &str = "host_vault";
const CLI_DEFAULT_SECRET_FALLBACK: &str = "encrypted_file";
const CLI_ALLOWED_SHARED_SECRET_BACKENDS: &[&str] = &["host_vault", "encrypted_file"];
const CLI_USES_PROTECTED_STORE: bool = true;
-const ENV_FILE_PATH: &str = "RADROOTS_ENV_FILE";
-const ENV_OUTPUT: &str = "RADROOTS_OUTPUT";
+const ENV_CLI_FILE_PATH: &str = "RADROOTS_CLI_ENV_FILE";
+const ENV_CLI_OUTPUT_FORMAT: &str = "RADROOTS_CLI_OUTPUT_FORMAT";
const ENV_CLI_LOG_FILTER: &str = "RADROOTS_CLI_LOGGING_FILTER";
const ENV_CLI_LOG_DIR: &str = "RADROOTS_CLI_LOGGING_OUTPUT_DIR";
const ENV_CLI_LOG_STDOUT: &str = "RADROOTS_CLI_LOGGING_STDOUT";
-const ENV_LOG_FILTER: &str = "RADROOTS_LOG_FILTER";
-const ENV_LOG_DIR: &str = "RADROOTS_LOG_DIR";
-const ENV_LOG_STDOUT: &str = "RADROOTS_LOG_STDOUT";
-const ENV_ACCOUNT: &str = "RADROOTS_ACCOUNT";
-const ENV_ACCOUNT_SECRET_BACKEND: &str = "RADROOTS_ACCOUNT_SECRET_BACKEND";
-const ENV_ACCOUNT_SECRET_FALLBACK: &str = "RADROOTS_ACCOUNT_SECRET_FALLBACK";
-const ENV_IDENTITY_PATH: &str = "RADROOTS_IDENTITY_PATH";
-const ENV_SIGNER: &str = "RADROOTS_SIGNER";
-const ENV_PUBLISH_MODE: &str = "RADROOTS_PUBLISH_MODE";
-const ENV_RELAYS: &str = "RADROOTS_RELAYS";
-const ENV_MYC_EXECUTABLE: &str = "RADROOTS_MYC_EXECUTABLE";
-const ENV_MYC_STATUS_TIMEOUT_MS: &str = "RADROOTS_MYC_STATUS_TIMEOUT_MS";
-const ENV_HYF_ENABLED: &str = "RADROOTS_HYF_ENABLED";
-const ENV_HYF_EXECUTABLE: &str = "RADROOTS_HYF_EXECUTABLE";
-const ENV_RPC_URL: &str = "RADROOTS_RPC_URL";
-const ENV_RPC_BEARER_TOKEN: &str = "RADROOTS_RPC_BEARER_TOKEN";
-const ENV_TRUSTED_RHI_WORKER_PUBKEYS: &str = "RADROOTS_TRUSTED_RHI_WORKER_PUBKEYS";
+const ENV_CLI_ACCOUNT_SELECTOR: &str = "RADROOTS_CLI_ACCOUNT_SELECTOR";
+const ENV_CLI_ACCOUNT_SECRET_BACKEND: &str = "RADROOTS_CLI_ACCOUNT_SECRET_BACKEND";
+const ENV_CLI_ACCOUNT_SECRET_FALLBACK: &str = "RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK";
+const ENV_CLI_IDENTITY_PATH: &str = "RADROOTS_CLI_IDENTITY_PATH";
+const ENV_CLI_SIGNER_BACKEND: &str = "RADROOTS_CLI_SIGNER_BACKEND";
+const ENV_CLI_PUBLISH_MODE: &str = "RADROOTS_CLI_PUBLISH_MODE";
+const ENV_CLI_RELAYS_URLS: &str = "RADROOTS_CLI_RELAYS_URLS";
+const ENV_CLI_MYC_EXECUTABLE: &str = "RADROOTS_CLI_MYC_EXECUTABLE";
+const ENV_CLI_MYC_STATUS_TIMEOUT_MS: &str = "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS";
+const ENV_CLI_HYF_ENABLED: &str = "RADROOTS_CLI_HYF_ENABLED";
+const ENV_CLI_HYF_EXECUTABLE: &str = "RADROOTS_CLI_HYF_EXECUTABLE";
+const ENV_CLI_RPC_URL: &str = "RADROOTS_CLI_RPC_URL";
+const ENV_CLI_RPC_BEARER_TOKEN: &str = "RADROOTS_CLI_RPC_BEARER_TOKEN";
+const ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS: &str = "RADROOTS_CLI_RHI_TRUSTED_WORKER_PUBKEYS";
const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
- ENV_OUTPUT,
+ ENV_CLI_OUTPUT_FORMAT,
ENV_CLI_LOG_FILTER,
ENV_CLI_LOG_DIR,
ENV_CLI_LOG_STDOUT,
ENV_CLI_PATHS_PROFILE,
ENV_CLI_PATHS_REPO_LOCAL_ROOT,
- ENV_LOG_FILTER,
- ENV_LOG_DIR,
- ENV_LOG_STDOUT,
- ENV_ACCOUNT,
- ENV_ACCOUNT_SECRET_BACKEND,
- ENV_ACCOUNT_SECRET_FALLBACK,
- ENV_IDENTITY_PATH,
- ENV_SIGNER,
- ENV_PUBLISH_MODE,
- ENV_RELAYS,
- ENV_MYC_EXECUTABLE,
- ENV_MYC_STATUS_TIMEOUT_MS,
- ENV_HYF_ENABLED,
- ENV_HYF_EXECUTABLE,
- ENV_RPC_URL,
- ENV_RPC_BEARER_TOKEN,
- ENV_TRUSTED_RHI_WORKER_PUBKEYS,
+ ENV_CLI_ACCOUNT_SELECTOR,
+ ENV_CLI_ACCOUNT_SECRET_BACKEND,
+ ENV_CLI_ACCOUNT_SECRET_FALLBACK,
+ ENV_CLI_IDENTITY_PATH,
+ ENV_CLI_SIGNER_BACKEND,
+ ENV_CLI_PUBLISH_MODE,
+ ENV_CLI_RELAYS_URLS,
+ ENV_CLI_MYC_EXECUTABLE,
+ ENV_CLI_MYC_STATUS_TIMEOUT_MS,
+ ENV_CLI_HYF_ENABLED,
+ ENV_CLI_HYF_EXECUTABLE,
+ ENV_CLI_RPC_URL,
+ ENV_CLI_RPC_BEARER_TOKEN,
+ ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -408,8 +403,13 @@ impl EnvFileValues {
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct CliConfigFile {
- relay: Option<RelayFileConfig>,
+ output: Option<OutputFileConfig>,
+ logging: Option<LoggingFileConfig>,
+ account: Option<AccountFileConfig>,
+ identity: Option<IdentityFileConfig>,
+ relays: Option<RelayFileConfig>,
publish: Option<PublishFileConfig>,
signer: Option<SignerFileConfig>,
myc: Option<MycFileConfig>,
@@ -420,44 +420,86 @@ struct CliConfigFile {
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct OutputFileConfig {
+ format: Option<String>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct LoggingFileConfig {
+ filter: Option<String>,
+ output_dir: Option<PathBuf>,
+ stdout: Option<bool>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct AccountFileConfig {
+ selector: Option<String>,
+ secret: Option<AccountSecretFileConfig>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct AccountSecretFileConfig {
+ backend: Option<String>,
+ fallback: Option<String>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+struct IdentityFileConfig {
+ path: Option<PathBuf>,
+}
+
+#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct RelayFileConfig {
urls: Option<Vec<String>>,
publish_policy: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct PublishFileConfig {
mode: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct RpcFileConfig {
url: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct RhiFileConfig {
trusted_worker_pubkeys: Option<Vec<String>>,
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct MycFileConfig {
executable: Option<PathBuf>,
status_timeout_ms: Option<u64>,
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct SignerFileConfig {
- mode: Option<String>,
+ backend: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
+#[serde(default, deny_unknown_fields)]
struct HyfFileConfig {
enabled: Option<bool>,
executable: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize)]
+#[serde(deny_unknown_fields)]
struct CapabilityBindingFileConfig {
capability: String,
provider: String,
@@ -547,41 +589,59 @@ impl RuntimeConfig {
.transpose()?
.flatten();
let app_config = load_cli_config_file(paths.app_config_path.as_path())?;
- let account_secret_backend = resolve_account_secret_backend(args, env, env_file)?
- .unwrap_or(RadrootsSecretBackend::HostVault(
- RadrootsHostVaultPolicy::desktop(),
- ));
- let account_secret_fallback = resolve_account_secret_fallback(args, env, env_file)?
- .unwrap_or(match account_secret_backend {
- RadrootsSecretBackend::HostVault(_) => Some(RadrootsSecretBackend::EncryptedFile),
- _ => None,
- });
+ let account_secret_backend = resolve_account_secret_backend(
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?
+ .unwrap_or(RadrootsSecretBackend::HostVault(
+ RadrootsHostVaultPolicy::desktop(),
+ ));
+ let account_secret_fallback = resolve_account_secret_fallback(
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?
+ .unwrap_or(match account_secret_backend {
+ RadrootsSecretBackend::HostVault(_) => Some(RadrootsSecretBackend::EncryptedFile),
+ _ => None,
+ });
let output = OutputConfig {
- format: resolve_output_format(args, env, env_file)?,
+ format: resolve_output_format(
+ args,
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ )?,
verbosity: resolve_verbosity(args)?,
color: !args.no_color,
dry_run: args.dry_run,
};
let logging = LoggingConfig {
- filter: args
- .log_filter
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_CLI_LOG_FILTER, ENV_LOG_FILTER]))
- .unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned()),
- directory: args.log_dir.clone().or_else(|| {
- env_value(env, env_file, &[ENV_CLI_LOG_DIR, ENV_LOG_DIR])
- .map(PathBuf::from)
- .or_else(|| Some(paths.app_logs_root.clone()))
- }),
- stdout: resolve_bool_pair(
- args.log_stdout,
- args.no_log_stdout,
- &[ENV_CLI_LOG_STDOUT, ENV_LOG_STDOUT],
- false,
+ filter: resolve_logging_filter(
+ args,
env,
env_file,
- "--log-stdout",
- "--no-log-stdout",
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ ),
+ directory: resolve_logging_directory(
+ args,
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
+ paths.app_logs_root.as_path(),
+ ),
+ stdout: resolve_logging_stdout(
+ args,
+ env,
+ env_file,
+ app_config.as_ref(),
+ workspace_config.as_ref(),
)?,
};
validate_logging_output_contract(&output, &logging)?;
@@ -599,7 +659,19 @@ impl RuntimeConfig {
selector: args
.account
.clone()
- .or_else(|| env_value(env, env_file, &[ENV_ACCOUNT])),
+ .or_else(|| env_value(env, env_file, &[ENV_CLI_ACCOUNT_SELECTOR]))
+ .or_else(|| {
+ app_config
+ .as_ref()
+ .and_then(|config| config.account.as_ref())
+ .and_then(|account| account.selector.clone())
+ })
+ .or_else(|| {
+ workspace_config
+ .as_ref()
+ .and_then(|config| config.account.as_ref())
+ .and_then(|account| account.selector.clone())
+ }),
store_path: paths
.shared_accounts_data_root
.join(DEFAULT_SHARED_ACCOUNTS_STORE_FILE),
@@ -621,7 +693,21 @@ impl RuntimeConfig {
path: args
.identity_path
.clone()
- .or_else(|| env_value(env, env_file, &[ENV_IDENTITY_PATH]).map(PathBuf::from))
+ .or_else(|| {
+ env_value(env, env_file, &[ENV_CLI_IDENTITY_PATH]).map(PathBuf::from)
+ })
+ .or_else(|| {
+ app_config
+ .as_ref()
+ .and_then(|config| config.identity.as_ref())
+ .and_then(|identity| identity.path.clone())
+ })
+ .or_else(|| {
+ workspace_config
+ .as_ref()
+ .and_then(|config| config.identity.as_ref())
+ .and_then(|identity| identity.path.clone())
+ })
.unwrap_or_else(|| paths.default_identity_path.clone()),
},
signer: resolve_signer_config(
@@ -828,7 +914,7 @@ fn resolve_rpc_config(
user_config: Option<&CliConfigFile>,
workspace_config: Option<&CliConfigFile>,
) -> Result<RpcConfig, RuntimeError> {
- let url = env_value(env, env_file, &[ENV_RPC_URL])
+ let url = env_value(env, env_file, &[ENV_CLI_RPC_URL])
.or_else(|| {
user_config
.and_then(|config| config.rpc.as_ref())
@@ -843,7 +929,7 @@ fn resolve_rpc_config(
Ok(RpcConfig {
url: validate_rpc_url(url.as_str())?,
- bridge_bearer_token: env_value(env, env_file, &[ENV_RPC_BEARER_TOKEN]),
+ bridge_bearer_token: env_value(env, env_file, &[ENV_CLI_RPC_BEARER_TOKEN]),
})
}
@@ -854,8 +940,8 @@ fn resolve_rhi_config(
workspace_config: Option<&CliConfigFile>,
) -> Result<RhiConfig, RuntimeError> {
let trusted_worker_pubkeys =
- if let Some(value) = env_value(env, env_file, &[ENV_TRUSTED_RHI_WORKER_PUBKEYS]) {
- parse_pubkey_env_value(value.as_str(), ENV_TRUSTED_RHI_WORKER_PUBKEYS)?
+ if let Some(value) = env_value(env, env_file, &[ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS]) {
+ parse_pubkey_env_value(value.as_str(), ENV_CLI_RHI_TRUSTED_WORKER_PUBKEYS)?
} else if let Some(values) = user_config
.and_then(|config| config.rhi.as_ref())
.and_then(|rhi| rhi.trusted_worker_pubkeys.clone())
@@ -1050,28 +1136,28 @@ fn resolve_relay_config(
});
}
- if let Some(value) = env_value(env, env_file, &[ENV_RELAYS]) {
+ if let Some(value) = env_value(env, env_file, &[ENV_CLI_RELAYS_URLS]) {
return Ok(RelayConfig {
- urls: parse_relay_env_value(value.as_str(), ENV_RELAYS)?,
+ urls: parse_relay_env_value(value.as_str(), ENV_CLI_RELAYS_URLS)?,
publish_policy,
source: RelayConfigSource::Environment,
});
}
- if let Some(relay) = user_config.and_then(|config| config.relay.as_ref()) {
+ if let Some(relay) = user_config.and_then(|config| config.relays.as_ref()) {
if let Some(urls) = relay.urls.clone() {
return Ok(RelayConfig {
- urls: normalize_relay_urls(urls, "user config [relay].urls")?,
+ urls: normalize_relay_urls(urls, "user config [relays].urls")?,
publish_policy,
source: RelayConfigSource::UserConfig,
});
}
}
- if let Some(relay) = workspace_config.and_then(|config| config.relay.as_ref()) {
+ if let Some(relay) = workspace_config.and_then(|config| config.relays.as_ref()) {
if let Some(urls) = relay.urls.clone() {
return Ok(RelayConfig {
- urls: normalize_relay_urls(urls, "workspace config [relay].urls")?,
+ urls: normalize_relay_urls(urls, "workspace config [relays].urls")?,
publish_policy,
source: RelayConfigSource::WorkspaceConfig,
});
@@ -1094,18 +1180,18 @@ fn resolve_signer_config(
) -> Result<SignerConfig, RuntimeError> {
let backend = if let Some(value) = args.signer.clone() {
parse_signer_mode("internal invocation signer mode", value)?
- } else if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_SIGNER]) {
+ } else if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_SIGNER_BACKEND]) {
parse_signer_mode(key.as_str(), value)?
} else if let Some(value) = user_config
.and_then(|config| config.signer.as_ref())
- .and_then(|signer| signer.mode.clone())
+ .and_then(|signer| signer.backend.clone())
{
- parse_signer_mode("user config [signer].mode", value)?
+ parse_signer_mode("user config [signer].backend", value)?
} else if let Some(value) = workspace_config
.and_then(|config| config.signer.as_ref())
- .and_then(|signer| signer.mode.clone())
+ .and_then(|signer| signer.backend.clone())
{
- parse_signer_mode("workspace config [signer].mode", value)?
+ parse_signer_mode("workspace config [signer].backend", value)?
} else {
SignerBackend::Local
};
@@ -1127,7 +1213,7 @@ fn resolve_publish_config(
});
}
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_PUBLISH_MODE]) {
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_PUBLISH_MODE]) {
return Ok(PublishConfig {
mode: parse_publish_mode(key.as_str(), value)?,
source: PublishModeSource::Environment,
@@ -1170,7 +1256,7 @@ fn resolve_myc_config(
let executable = args
.myc_executable
.clone()
- .or_else(|| env_value(env, env_file, &[ENV_MYC_EXECUTABLE]).map(PathBuf::from))
+ .or_else(|| env_value(env, env_file, &[ENV_CLI_MYC_EXECUTABLE]).map(PathBuf::from))
.or_else(|| {
user_config
.and_then(|config| config.myc.as_ref())
@@ -1206,10 +1292,9 @@ fn resolve_myc_status_timeout_ms(
return validate_myc_status_timeout_ms("--myc-status-timeout-ms", value);
}
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_MYC_STATUS_TIMEOUT_MS]) {
- let parsed = value.trim().parse::<u64>().map_err(|err| {
- RuntimeError::Config(format!("{key} must be an integer millisecond value: {err}"))
- })?;
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_MYC_STATUS_TIMEOUT_MS]) {
+ let parsed = parse_u64_value(key.as_str(), value.as_str())
+ .map_err(|err| RuntimeError::Config(err.to_string()))?;
return validate_myc_status_timeout_ms(key.as_str(), parsed);
}
@@ -1257,7 +1342,7 @@ fn resolve_hyf_enabled(
(false, false) => {}
}
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_HYF_ENABLED]) {
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_HYF_ENABLED]) {
return parse_bool_env(key.as_str(), value.as_str());
}
@@ -1287,7 +1372,7 @@ fn resolve_hyf_executable(
) -> PathBuf {
args.hyf_executable
.clone()
- .or_else(|| env_value(env, env_file, &[ENV_HYF_EXECUTABLE]).map(PathBuf::from))
+ .or_else(|| env_value(env, env_file, &[ENV_CLI_HYF_EXECUTABLE]).map(PathBuf::from))
.or_else(|| {
user_config
.and_then(|config| config.hyf.as_ref())
@@ -1306,14 +1391,14 @@ fn resolve_relay_publish_policy(
workspace_config: Option<&CliConfigFile>,
) -> Result<Option<RelayPublishPolicy>, RuntimeError> {
if let Some(value) = user_config
- .and_then(|config| config.relay.as_ref())
+ .and_then(|config| config.relays.as_ref())
.and_then(|relay| relay.publish_policy.as_deref())
{
return parse_relay_publish_policy(value).map(Some);
}
if let Some(value) = workspace_config
- .and_then(|config| config.relay.as_ref())
+ .and_then(|config| config.relays.as_ref())
.and_then(|relay| relay.publish_policy.as_deref())
{
return parse_relay_publish_policy(value).map(Some);
@@ -1326,7 +1411,7 @@ fn parse_relay_publish_policy(value: &str) -> Result<RelayPublishPolicy, Runtime
match value.trim().to_ascii_lowercase().as_str() {
"any" => Ok(RelayPublishPolicy::Any),
other => Err(RuntimeError::Config(format!(
- "[relay].publish_policy must be `any`, got `{other}`"
+ "[relays].publish_policy must be `any`, got `{other}`"
))),
}
}
@@ -1393,7 +1478,7 @@ fn validate_relay_url(value: &str, source: &str) -> Result<String, RuntimeError>
fn resolve_env_file_path(args: &RuntimeInvocationArgs, env: &dyn Environment) -> Option<PathBuf> {
args.env_file
.clone()
- .or_else(|| env.var(ENV_FILE_PATH).map(PathBuf::from))
+ .or_else(|| env.var(ENV_CLI_FILE_PATH).map(PathBuf::from))
.or_else(|| {
let default_path = PathBuf::from(DEFAULT_ENV_PATH);
default_path.exists().then_some(default_path)
@@ -1404,6 +1489,8 @@ fn resolve_output_format(
args: &RuntimeInvocationArgs,
env: &dyn Environment,
env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
) -> Result<OutputFormat, RuntimeError> {
if args.output_format.is_some() && (args.json || args.ndjson) {
return Err(RuntimeError::Config(
@@ -1423,8 +1510,22 @@ fn resolve_output_format(
(None, false, false) => {}
(Some(_), true, false) | (Some(_), false, true) => unreachable!(),
}
- match env_value(env, env_file, &[ENV_OUTPUT]) {
- Some(value) => parse_output_format(value.as_str()),
+ if let Some(value) = env_value(env, env_file, &[ENV_CLI_OUTPUT_FORMAT]) {
+ return parse_output_format(value.as_str());
+ }
+
+ if let Some(value) = user_config
+ .and_then(|config| config.output.as_ref())
+ .and_then(|output| output.format.as_deref())
+ {
+ return parse_output_format(value);
+ }
+
+ match workspace_config
+ .and_then(|config| config.output.as_ref())
+ .and_then(|output| output.format.as_deref())
+ {
+ Some(value) => parse_output_format(value),
None => Ok(OutputFormat::Human),
}
}
@@ -1470,13 +1571,85 @@ fn resolve_interaction_config(
}
}
+fn resolve_logging_filter(
+ args: &RuntimeInvocationArgs,
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+) -> String {
+ args.log_filter
+ .clone()
+ .or_else(|| env_value(env, env_file, &[ENV_CLI_LOG_FILTER]))
+ .or_else(|| {
+ user_config
+ .and_then(|config| config.logging.as_ref())
+ .and_then(|logging| logging.filter.clone())
+ })
+ .or_else(|| {
+ workspace_config
+ .and_then(|config| config.logging.as_ref())
+ .and_then(|logging| logging.filter.clone())
+ })
+ .unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned())
+}
+
+fn resolve_logging_directory(
+ args: &RuntimeInvocationArgs,
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+ default_logs_root: &Path,
+) -> Option<PathBuf> {
+ args.log_dir
+ .clone()
+ .or_else(|| env_value(env, env_file, &[ENV_CLI_LOG_DIR]).map(PathBuf::from))
+ .or_else(|| {
+ user_config
+ .and_then(|config| config.logging.as_ref())
+ .and_then(|logging| logging.output_dir.clone())
+ })
+ .or_else(|| {
+ workspace_config
+ .and_then(|config| config.logging.as_ref())
+ .and_then(|logging| logging.output_dir.clone())
+ })
+ .or_else(|| Some(default_logs_root.to_path_buf()))
+}
+
+fn resolve_logging_stdout(
+ args: &RuntimeInvocationArgs,
+ env: &dyn Environment,
+ env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
+) -> Result<bool, RuntimeError> {
+ resolve_bool_pair(
+ args.log_stdout,
+ args.no_log_stdout,
+ &[ENV_CLI_LOG_STDOUT],
+ user_config
+ .and_then(|config| config.logging.as_ref())
+ .and_then(|logging| logging.stdout),
+ workspace_config
+ .and_then(|config| config.logging.as_ref())
+ .and_then(|logging| logging.stdout),
+ false,
+ env,
+ env_file,
+ "--log-stdout",
+ "--no-log-stdout",
+ )
+}
+
fn validate_logging_output_contract(
output: &OutputConfig,
logging: &LoggingConfig,
) -> Result<(), RuntimeError> {
if logging.stdout && matches!(output.format, OutputFormat::Json | OutputFormat::Ndjson) {
return Err(RuntimeError::Config(format!(
- "stdout logging cannot be used with {} output; unset {ENV_CLI_LOG_STDOUT}/{ENV_LOG_STDOUT} or use --no-log-stdout",
+ "stdout logging cannot be used with {} output; unset {ENV_CLI_LOG_STDOUT} or use --no-log-stdout",
output.format.as_str()
)));
}
@@ -1488,6 +1661,8 @@ fn resolve_bool_pair(
positive_flag: bool,
negative_flag: bool,
env_keys: &[&str],
+ user_value: Option<bool>,
+ workspace_value: Option<bool>,
default: bool,
env: &dyn Environment,
env_file: &EnvFileValues,
@@ -1502,7 +1677,7 @@ fn resolve_bool_pair(
(false, true) => Ok(false),
(false, false) => match env_value_entry(env, env_file, env_keys) {
Some((key, value)) => parse_bool_env(key.as_str(), value.as_str()),
- None => Ok(default),
+ None => Ok(user_value.or(workspace_value).unwrap_or(default)),
},
}
}
@@ -1540,50 +1715,9 @@ fn load_env_file_values(path: Option<&Path>) -> Result<EnvFileValues, RuntimeErr
}
fn parse_env_file_values(raw: &str, path: &Path) -> Result<EnvFileValues, RuntimeError> {
- let mut values = BTreeMap::new();
-
- for (index, line) in raw.lines().enumerate() {
- let trimmed = line.trim();
- if trimmed.is_empty() || trimmed.starts_with('#') {
- continue;
- }
- let Some((key, value)) = trimmed.split_once('=') else {
- return Err(RuntimeError::Config(format!(
- "invalid env file {} line {}: expected KEY=VALUE",
- path.display(),
- index + 1
- )));
- };
- let key = key.trim();
- if key.is_empty() {
- return Err(RuntimeError::Config(format!(
- "invalid env file {} line {}: empty key",
- path.display(),
- index + 1
- )));
- }
- if !SUPPORTED_ENV_FILE_KEYS.contains(&key) {
- return Err(RuntimeError::Config(format!(
- "invalid env file {} line {}: unknown environment variable `{key}`",
- path.display(),
- index + 1
- )));
- }
- values.insert(key.to_owned(), normalize_env_value(value.trim()));
- }
-
- Ok(EnvFileValues(values))
-}
-
-fn normalize_env_value(value: &str) -> String {
- if value.len() >= 2 {
- let first = value.as_bytes()[0];
- let last = value.as_bytes()[value.len() - 1];
- if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
- return value[1..value.len() - 1].to_owned();
- }
- }
- value.to_owned()
+ parse_strict_env_file(raw, path, SUPPORTED_ENV_FILE_KEYS)
+ .map(|values| EnvFileValues(values.into_inner()))
+ .map_err(|err| RuntimeError::Config(err.to_string()))
}
fn parse_output_format(value: &str) -> Result<OutputFormat, RuntimeError> {
@@ -1592,7 +1726,7 @@ fn parse_output_format(value: &str) -> Result<OutputFormat, RuntimeError> {
"json" => Ok(OutputFormat::Json),
"ndjson" => Ok(OutputFormat::Ndjson),
other => Err(RuntimeError::Config(format!(
- "{ENV_OUTPUT} must be `human`, `json`, or `ndjson`, got `{other}`"
+ "{ENV_CLI_OUTPUT_FORMAT} must be `human`, `json`, or `ndjson`, got `{other}`"
))),
}
}
@@ -1620,22 +1754,60 @@ fn parse_publish_mode(source: &str, value: String) -> Result<PublishMode, Runtim
}
fn resolve_account_secret_backend(
- _args: &RuntimeInvocationArgs,
env: &dyn Environment,
env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
) -> Result<Option<RadrootsSecretBackend>, RuntimeError> {
- env_value_entry(env, env_file, &[ENV_ACCOUNT_SECRET_BACKEND])
- .map(|(key, value)| parse_account_secret_backend(key.as_str(), value.as_str()))
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_ACCOUNT_SECRET_BACKEND]) {
+ return parse_account_secret_backend(key.as_str(), value.as_str()).map(Some);
+ }
+
+ if let Some(value) = user_config
+ .and_then(|config| config.account.as_ref())
+ .and_then(|account| account.secret.as_ref())
+ .and_then(|secret| secret.backend.as_deref())
+ {
+ return parse_account_secret_backend("user config [account.secret].backend", value)
+ .map(Some);
+ }
+
+ workspace_config
+ .and_then(|config| config.account.as_ref())
+ .and_then(|account| account.secret.as_ref())
+ .and_then(|secret| secret.backend.as_deref())
+ .map(|value| {
+ parse_account_secret_backend("workspace config [account.secret].backend", value)
+ })
.transpose()
}
fn resolve_account_secret_fallback(
- _args: &RuntimeInvocationArgs,
env: &dyn Environment,
env_file: &EnvFileValues,
+ user_config: Option<&CliConfigFile>,
+ workspace_config: Option<&CliConfigFile>,
) -> Result<Option<Option<RadrootsSecretBackend>>, RuntimeError> {
- env_value_entry(env, env_file, &[ENV_ACCOUNT_SECRET_FALLBACK])
- .map(|(key, value)| parse_account_secret_fallback(key.as_str(), value.as_str()))
+ if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_ACCOUNT_SECRET_FALLBACK]) {
+ return parse_account_secret_fallback(key.as_str(), value.as_str()).map(Some);
+ }
+
+ if let Some(value) = user_config
+ .and_then(|config| config.account.as_ref())
+ .and_then(|account| account.secret.as_ref())
+ .and_then(|secret| secret.fallback.as_deref())
+ {
+ return parse_account_secret_fallback("user config [account.secret].fallback", value)
+ .map(Some);
+ }
+
+ workspace_config
+ .and_then(|config| config.account.as_ref())
+ .and_then(|account| account.secret.as_ref())
+ .and_then(|secret| secret.fallback.as_deref())
+ .map(|value| {
+ parse_account_secret_fallback("workspace config [account.secret].fallback", value)
+ })
.transpose()
}
@@ -1671,23 +1843,19 @@ fn parse_account_secret_backend(
}
fn parse_bool_env(key: &str, value: &str) -> Result<bool, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "1" | "true" | "yes" | "on" => Ok(true),
- "0" | "false" | "no" | "off" => Ok(false),
- other => Err(RuntimeError::Config(format!(
- "{key} must be a boolean value, got `{other}`"
- ))),
- }
+ parse_bool_value(key, value).map_err(|err| RuntimeError::Config(err.to_string()))
}
#[cfg(test)]
mod tests {
use super::{
AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
- CapabilityBindingSource, CapabilityBindingTargetKind, EnvFileValues, Environment,
- HyfConfig, INFERENCE_HYF_STDIO_CAPABILITY, InteractionConfig, OutputConfig, OutputFormat,
- PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfigSource,
- RelayPublishPolicy, RuntimeConfig, SignerBackend, Verbosity, parse_env_file_values,
+ CapabilityBindingSource, CapabilityBindingTargetKind, DEFAULT_HYF_EXECUTABLE,
+ DEFAULT_LOG_FILTER, DEFAULT_MYC_STATUS_TIMEOUT_MS, DEFAULT_RPC_URL, EnvFileValues,
+ Environment, HyfConfig, INFERENCE_HYF_STDIO_CAPABILITY, InteractionConfig, OutputConfig,
+ OutputFormat, PathsConfig, PublishConfig, PublishMode, PublishModeSource,
+ RelayConfigSource, RelayPublishPolicy, RuntimeConfig, SignerBackend, Verbosity,
+ parse_env_file_values,
};
use crate::cli::global::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
@@ -1805,23 +1973,35 @@ mod tests {
..runtime_args()
};
let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_OUTPUT".to_owned(), "human".to_owned()),
- ("RADROOTS_LOG_FILTER".to_owned(), "trace".to_owned()),
- ("RADROOTS_LOG_STDOUT".to_owned(), "false".to_owned()),
+ ("RADROOTS_CLI_OUTPUT_FORMAT".to_owned(), "human".to_owned()),
+ ("RADROOTS_CLI_LOGGING_FILTER".to_owned(), "trace".to_owned()),
+ ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "false".to_owned()),
(
- "RADROOTS_IDENTITY_PATH".to_owned(),
+ "RADROOTS_CLI_IDENTITY_PATH".to_owned(),
"env-identity.json".to_owned(),
),
- ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()),
- ("RADROOTS_PUBLISH_MODE".to_owned(), "radrootsd".to_owned()),
- ("RADROOTS_RELAYS".to_owned(), "wss://relay.env".to_owned()),
- ("RADROOTS_MYC_EXECUTABLE".to_owned(), "env-myc".to_owned()),
+ ("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
(
- "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
+ "radrootsd".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_RELAYS_URLS".to_owned(),
+ "wss://relay.env".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_MYC_EXECUTABLE".to_owned(),
+ "env-myc".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS".to_owned(),
"9000".to_owned(),
),
- ("RADROOTS_HYF_ENABLED".to_owned(), "false".to_owned()),
- ("RADROOTS_HYF_EXECUTABLE".to_owned(), "env-hyfd".to_owned()),
+ ("RADROOTS_CLI_HYF_ENABLED".to_owned(), "false".to_owned()),
+ (
+ "RADROOTS_CLI_HYF_EXECUTABLE".to_owned(),
+ "env-hyfd".to_owned(),
+ ),
]));
let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
@@ -1933,31 +2113,46 @@ mod tests {
fn environment_values_fill_missing_flags() {
let args = runtime_args();
let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_OUTPUT".to_owned(), "json".to_owned()),
+ ("RADROOTS_CLI_OUTPUT_FORMAT".to_owned(), "json".to_owned()),
(
- "RADROOTS_LOG_FILTER".to_owned(),
+ "RADROOTS_CLI_LOGGING_FILTER".to_owned(),
"debug,cli=trace".to_owned(),
),
- ("RADROOTS_LOG_DIR".to_owned(), "logs/runtime".to_owned()),
- ("RADROOTS_LOG_STDOUT".to_owned(), "false".to_owned()),
- ("RADROOTS_ACCOUNT".to_owned(), "acct_demo".to_owned()),
(
- "RADROOTS_IDENTITY_PATH".to_owned(),
+ "RADROOTS_CLI_LOGGING_OUTPUT_DIR".to_owned(),
+ "logs/runtime".to_owned(),
+ ),
+ ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "false".to_owned()),
+ (
+ "RADROOTS_CLI_ACCOUNT_SELECTOR".to_owned(),
+ "acct_demo".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_IDENTITY_PATH".to_owned(),
"state/identity.json".to_owned(),
),
- ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()),
- ("RADROOTS_PUBLISH_MODE".to_owned(), "radrootsd".to_owned()),
+ ("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
(
- "RADROOTS_RELAYS".to_owned(),
+ "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
+ "radrootsd".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_RELAYS_URLS".to_owned(),
"wss://relay.one,wss://relay.two".to_owned(),
),
- ("RADROOTS_MYC_EXECUTABLE".to_owned(), "bin/myc".to_owned()),
(
- "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "RADROOTS_CLI_MYC_EXECUTABLE".to_owned(),
+ "bin/myc".to_owned(),
+ ),
+ (
+ "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS".to_owned(),
"3500".to_owned(),
),
- ("RADROOTS_HYF_ENABLED".to_owned(), "true".to_owned()),
- ("RADROOTS_HYF_EXECUTABLE".to_owned(), "bin/hyfd".to_owned()),
+ ("RADROOTS_CLI_HYF_ENABLED".to_owned(), "true".to_owned()),
+ (
+ "RADROOTS_CLI_HYF_EXECUTABLE".to_owned(),
+ "bin/hyfd".to_owned(),
+ ),
]));
let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
@@ -2023,6 +2218,143 @@ mod tests {
}
#[test]
+ fn old_process_environment_names_have_no_effect() {
+ let args = runtime_args();
+ let env = MapEnvironment::new(BTreeMap::from([
+ ("RADROOTS_OUTPUT".to_owned(), "json".to_owned()),
+ ("RADROOTS_LOG_FILTER".to_owned(), "trace".to_owned()),
+ ("RADROOTS_LOG_DIR".to_owned(), "logs/old".to_owned()),
+ ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()),
+ ("RADROOTS_ACCOUNT".to_owned(), "old_account".to_owned()),
+ (
+ "RADROOTS_ACCOUNT_SECRET_BACKEND".to_owned(),
+ "encrypted_file".to_owned(),
+ ),
+ (
+ "RADROOTS_ACCOUNT_SECRET_FALLBACK".to_owned(),
+ "none".to_owned(),
+ ),
+ (
+ "RADROOTS_IDENTITY_PATH".to_owned(),
+ "old-identity.json".to_owned(),
+ ),
+ ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()),
+ ("RADROOTS_PUBLISH_MODE".to_owned(), "radrootsd".to_owned()),
+ (
+ "RADROOTS_RELAYS".to_owned(),
+ "wss://old-relay.example".to_owned(),
+ ),
+ ("RADROOTS_MYC_EXECUTABLE".to_owned(), "old-myc".to_owned()),
+ (
+ "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "9999".to_owned(),
+ ),
+ ("RADROOTS_HYF_ENABLED".to_owned(), "true".to_owned()),
+ ("RADROOTS_HYF_EXECUTABLE".to_owned(), "old-hyfd".to_owned()),
+ (
+ "RADROOTS_RPC_URL".to_owned(),
+ "http://127.0.0.1:9".to_owned(),
+ ),
+ (
+ "RADROOTS_RPC_BEARER_TOKEN".to_owned(),
+ "old-token".to_owned(),
+ ),
+ (
+ "RADROOTS_TRUSTED_RHI_WORKER_PUBKEYS".to_owned(),
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_owned(),
+ ),
+ ]));
+
+ let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
+ .expect("resolve runtime config");
+
+ assert_eq!(resolved.output.format, OutputFormat::Human);
+ assert_eq!(resolved.logging.filter, DEFAULT_LOG_FILTER);
+ assert!(!resolved.logging.stdout);
+ assert_eq!(resolved.account.selector, None);
+ assert_eq!(
+ resolved.account.secret_backend,
+ RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop())
+ );
+ assert_eq!(
+ resolved.account.secret_fallback,
+ Some(RadrootsSecretBackend::EncryptedFile)
+ );
+ assert_eq!(resolved.signer.backend, SignerBackend::Local);
+ assert_eq!(resolved.publish.mode, PublishMode::NostrRelay);
+ assert_eq!(resolved.relay.urls, Vec::<String>::new());
+ assert_eq!(resolved.myc.executable, PathBuf::from("myc"));
+ assert_eq!(
+ resolved.myc.status_timeout_ms,
+ DEFAULT_MYC_STATUS_TIMEOUT_MS
+ );
+ assert!(!resolved.hyf.enabled);
+ assert_eq!(
+ resolved.hyf.executable,
+ PathBuf::from(DEFAULT_HYF_EXECUTABLE)
+ );
+ assert_eq!(resolved.rpc.url, DEFAULT_RPC_URL);
+ assert_eq!(resolved.rpc.bridge_bearer_token, None);
+ assert_eq!(resolved.rhi.trusted_worker_pubkeys, Vec::<String>::new());
+ }
+
+ #[test]
+ fn toml_output_logging_account_and_identity_config_resolve() {
+ let temp = tempdir().expect("tempdir");
+ let workspace_root = temp.path().join("workspace");
+ let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
+ let app_config_dir = repo_local_root.join("config/apps/cli");
+ let user_home = temp.path().join("home");
+ fs::create_dir_all(&app_config_dir).expect("app config dir");
+ fs::write(
+ app_config_dir.join("config.toml"),
+ r#"
+[output]
+format = "json"
+
+[logging]
+filter = "debug,cli=trace"
+output_dir = "logs/from-toml"
+stdout = false
+
+[account]
+selector = "acct_from_toml"
+
+[account.secret]
+backend = "encrypted_file"
+fallback = "none"
+
+[identity]
+path = "identity/from-toml.json"
+"#,
+ )
+ .expect("write user config");
+
+ let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
+ let resolved =
+ RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
+ .expect("resolve toml config");
+
+ assert_eq!(resolved.output.format, OutputFormat::Json);
+ assert_eq!(resolved.logging.filter, "debug,cli=trace");
+ assert_eq!(
+ resolved.logging.directory,
+ Some(PathBuf::from("logs/from-toml"))
+ );
+ assert!(!resolved.logging.stdout);
+ assert_eq!(resolved.account.selector.as_deref(), Some("acct_from_toml"));
+ assert_eq!(
+ resolved.account.secret_backend,
+ RadrootsSecretBackend::EncryptedFile
+ );
+ assert_eq!(resolved.account.secret_fallback, None);
+ assert_eq!(
+ resolved.identity.path,
+ PathBuf::from("identity/from-toml.json")
+ );
+ }
+
+ #[test]
fn conflicting_boolean_flags_fail() {
let args = RuntimeInvocationArgs {
log_stdout: true,
@@ -2138,12 +2470,12 @@ mod tests {
.expect_err("json stdout logging from env should fail");
let message = error.to_string();
assert!(message.contains("RADROOTS_CLI_LOGGING_STDOUT"));
- assert!(message.contains("RADROOTS_LOG_STDOUT"));
+ assert!(message.contains("RADROOTS_CLI_LOGGING_STDOUT"));
let ndjson_env_args = runtime_args();
let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_OUTPUT".to_owned(), "ndjson".to_owned()),
- ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()),
+ ("RADROOTS_CLI_OUTPUT_FORMAT".to_owned(), "ndjson".to_owned()),
+ ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "true".to_owned()),
]));
let error =
RuntimeConfig::resolve_with_env_file(&ndjson_env_args, &env, &EnvFileValues::default())
@@ -2159,7 +2491,7 @@ mod tests {
..runtime_args()
};
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_LOG_STDOUT".to_owned(),
+ "RADROOTS_CLI_LOGGING_STDOUT".to_owned(),
"true".to_owned(),
)]));
@@ -2173,28 +2505,32 @@ mod tests {
fn invalid_environment_value_fails() {
let args = runtime_args();
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_LOG_STDOUT".to_owned(),
+ "RADROOTS_CLI_LOGGING_STDOUT".to_owned(),
"maybe".to_owned(),
)]));
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect_err("invalid bool");
- assert!(error.to_string().contains("RADROOTS_LOG_STDOUT"));
+ assert!(error.to_string().contains("RADROOTS_CLI_LOGGING_STDOUT"));
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
+ "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS".to_owned(),
"slow".to_owned(),
)]));
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect_err("invalid myc timeout");
- assert!(error.to_string().contains("RADROOTS_MYC_STATUS_TIMEOUT_MS"));
+ assert!(
+ error
+ .to_string()
+ .contains("RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS")
+ );
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_PUBLISH_MODE".to_owned(),
+ "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
"relay".to_owned(),
)]));
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect_err("invalid publish mode");
- assert!(error.to_string().contains("RADROOTS_PUBLISH_MODE"));
+ assert!(error.to_string().contains("RADROOTS_CLI_PUBLISH_MODE"));
assert!(error.to_string().contains("nostr_relay"));
assert!(error.to_string().contains("radrootsd"));
@@ -2214,19 +2550,19 @@ mod tests {
let env = MapEnvironment::new(BTreeMap::new());
let env_file = parse_env_file_values(
r#"
-RADROOTS_OUTPUT=json
+RADROOTS_CLI_OUTPUT_FORMAT=json
RADROOTS_CLI_LOGGING_FILTER="debug,radroots_cli=trace"
RADROOTS_CLI_LOGGING_OUTPUT_DIR=/tmp/radroots-cli-logs
RADROOTS_CLI_LOGGING_STDOUT=false
-RADROOTS_ACCOUNT=acct_env_file
-RADROOTS_IDENTITY_PATH=state/identity.json
-RADROOTS_SIGNER=myc
-RADROOTS_PUBLISH_MODE=radrootsd
-RADROOTS_RELAYS=wss://relay.env-file
-RADROOTS_MYC_EXECUTABLE=bin/myc
-RADROOTS_MYC_STATUS_TIMEOUT_MS=4500
-RADROOTS_HYF_ENABLED=true
-RADROOTS_HYF_EXECUTABLE=bin/hyfd
+RADROOTS_CLI_ACCOUNT_SELECTOR=acct_env_file
+RADROOTS_CLI_IDENTITY_PATH=state/identity.json
+RADROOTS_CLI_SIGNER_BACKEND=myc
+RADROOTS_CLI_PUBLISH_MODE=radrootsd
+RADROOTS_CLI_RELAYS_URLS=wss://relay.env-file
+RADROOTS_CLI_MYC_EXECUTABLE=bin/myc
+RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS=4500
+RADROOTS_CLI_HYF_ENABLED=true
+RADROOTS_CLI_HYF_EXECUTABLE=bin/hyfd
"#,
Path::new(".env.test"),
)
@@ -2271,7 +2607,7 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd
..runtime_args()
};
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_OUTPUT".to_owned(),
+ "RADROOTS_CLI_OUTPUT_FORMAT".to_owned(),
"json".to_owned(),
)]));
@@ -2327,8 +2663,8 @@ RADROOTS_HYF_EXECUTABLE=bin/hyfd
fn process_environment_overrides_env_file_values() {
let args = runtime_args();
let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_LOG_FILTER".to_owned(), "info".to_owned()),
- ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()),
+ ("RADROOTS_CLI_LOGGING_FILTER".to_owned(), "info".to_owned()),
+ ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "true".to_owned()),
]));
let env_file = parse_env_file_values(
r#"
@@ -2357,12 +2693,12 @@ RADROOTS_CLI_LOGGING_STDOUT=false
fs::create_dir_all(&app_config_dir).expect("app config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.workspace\"]\npublish_policy = \"any\"\n",
+ "[relays]\nurls = [\"wss://relay.workspace\"]\npublish_policy = \"any\"\n",
)
.expect("write workspace config");
fs::write(
app_config_dir.join("config.toml"),
- "[relay]\nurls = [\"wss://relay.user\", \"wss://relay.workspace\"]\n",
+ "[relays]\nurls = [\"wss://relay.user\", \"wss://relay.workspace\"]\n",
)
.expect("write user config");
@@ -2427,7 +2763,10 @@ RADROOTS_CLI_LOGGING_STDOUT=false
workspace_root.clone(),
repo_local_root.clone(),
user_home.clone(),
- BTreeMap::from([("RADROOTS_PUBLISH_MODE".to_owned(), "radrootsd".to_owned())]),
+ BTreeMap::from([(
+ "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
+ "radrootsd".to_owned(),
+ )]),
);
let args = RuntimeInvocationArgs {
publish_mode: Some("nostr_relay".to_owned()),
@@ -2447,7 +2786,10 @@ RADROOTS_CLI_LOGGING_STDOUT=false
workspace_root.clone(),
repo_local_root.clone(),
user_home.clone(),
- BTreeMap::from([("RADROOTS_PUBLISH_MODE".to_owned(), "radrootsd".to_owned())]),
+ BTreeMap::from([(
+ "RADROOTS_CLI_PUBLISH_MODE".to_owned(),
+ "radrootsd".to_owned(),
+ )]),
);
let resolved =
RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
@@ -2624,12 +2966,12 @@ RADROOTS_CLI_LOGGING_STDOUT=false
fs::create_dir_all(&app_config_dir).expect("app config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[signer]\nmode = \"myc\"\n",
+ "[signer]\nbackend = \"myc\"\n",
)
.expect("write workspace config");
fs::write(
app_config_dir.join("config.toml"),
- "[signer]\nmode = \"local\"\n",
+ "[signer]\nbackend = \"local\"\n",
)
.expect("write user config");
@@ -2651,7 +2993,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false
fs::create_dir_all(&app_config_dir).expect("app config dir");
fs::write(
app_config_dir.join("config.toml"),
- "[signer]\nmode = \"local\"\n",
+ "[signer]\nbackend = \"local\"\n",
)
.expect("write user config");
@@ -2659,7 +3001,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false
workspace_root,
repo_local_root,
user_home,
- BTreeMap::from([("RADROOTS_SIGNER".to_owned(), "myc".to_owned())]),
+ BTreeMap::from([("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned())]),
);
let args = runtime_args();
@@ -2677,7 +3019,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false
fs::create_dir_all(&repo_local_root).expect("workspace config dir");
fs::write(
repo_local_root.join("config.toml"),
- "[signer]\nmode = \"remote\"\n",
+ "[signer]\nbackend = \"remote\"\n",
)
.expect("write workspace config");
@@ -2687,7 +3029,7 @@ RADROOTS_CLI_LOGGING_STDOUT=false
let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
.expect_err("invalid signer mode");
let message = error.to_string();
- assert!(message.contains("workspace config [signer].mode"));
+ assert!(message.contains("workspace config [signer].backend"));
assert!(!message.contains("--signer"));
}
@@ -2889,7 +3231,7 @@ target = "workflow-default"
#[test]
fn relay_env_value_rejects_empty_entries() {
let env = MapEnvironment::new(BTreeMap::from([(
- super::ENV_RELAYS.to_owned(),
+ super::ENV_CLI_RELAYS_URLS.to_owned(),
"wss://relay.example,,wss://relay-two.example".to_owned(),
)]));
let error =
@@ -3130,10 +3472,74 @@ RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT=.local/radroots/dev
}
#[test]
+ fn old_env_file_variable_fails() {
+ let error = parse_env_file_values("RADROOTS_OUTPUT=json\n", Path::new(".env.test"))
+ .expect_err("old env variable should fail");
+ assert!(
+ error
+ .to_string()
+ .contains("unknown environment variable `RADROOTS_OUTPUT`")
+ );
+ }
+
+ #[test]
+ fn duplicate_env_file_variable_fails() {
+ let error = parse_env_file_values(
+ "RADROOTS_CLI_OUTPUT_FORMAT=json\nRADROOTS_CLI_OUTPUT_FORMAT=human\n",
+ Path::new(".env.test"),
+ )
+ .expect_err("duplicate env variable should fail");
+ assert!(
+ error
+ .to_string()
+ .contains("duplicate environment variable `RADROOTS_CLI_OUTPUT_FORMAT`")
+ );
+ }
+
+ #[test]
+ fn old_toml_groups_and_fields_fail() {
+ let temp = tempdir().expect("tempdir");
+ let workspace_root = temp.path().join("workspace");
+ let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
+ let user_home = temp.path().join("home");
+ fs::create_dir_all(&repo_local_root).expect("workspace config dir");
+
+ for (raw, expected) in [
+ (
+ "[relay]\nurls = [\"wss://relay.old\"]\n",
+ "unknown field `relay`",
+ ),
+ ("[signer]\nmode = \"local\"\n", "unknown field `mode`"),
+ (
+ "[relays]\nurls = [\"wss://relay.example\"]\nextra = true\n",
+ "unknown field `extra`",
+ ),
+ ] {
+ fs::write(repo_local_root.join("config.toml"), raw).expect("write config");
+ let env = repo_local_env(
+ workspace_root.clone(),
+ repo_local_root.clone(),
+ user_home.clone(),
+ BTreeMap::new(),
+ );
+ let error = RuntimeConfig::resolve_with_env_file(
+ &runtime_args(),
+ &env,
+ &EnvFileValues::default(),
+ )
+ .expect_err("old toml shape should fail");
+ assert!(
+ error.to_string().contains(expected),
+ "expected {expected}, got {error}"
+ );
+ }
+ }
+
+ #[test]
fn env_output_accepts_ndjson() {
let args = runtime_args();
let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_OUTPUT".to_owned(),
+ "RADROOTS_CLI_OUTPUT_FORMAT".to_owned(),
"ndjson".to_owned(),
)]));
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -531,9 +531,9 @@ fn account_attach_secret_reports_structured_validation_failures() {
let mut unavailable_command = sandbox.command();
unavailable_command
- .env("RADROOTS_ACCOUNT_SECRET_BACKEND", "host_vault")
- .env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "none")
- .env("RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE", "false")
+ .env("RADROOTS_CLI_ACCOUNT_SECRET_BACKEND", "host_vault")
+ .env("RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK", "none")
+ .env("RADROOTS_CLI_ACCOUNT_HOST_VAULT_AVAILABLE", "false")
.args([
"--format",
"json",
@@ -2569,7 +2569,7 @@ fn myc_listing_publish_does_not_fallback_to_local_account() {
fn configure_myc_mode(sandbox: &RadrootsCliSandbox, executable: &Path) {
sandbox.write_app_config(&format!(
- "[signer]\nmode = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
+ "[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
toml_string(executable.display().to_string().as_str())
));
}
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
@@ -152,8 +152,8 @@ impl RadrootsCliSandbox {
fn apply_base_env(&self, command: &mut Command) {
command.env("RADROOTS_CLI_PATHS_PROFILE", "repo_local");
command.env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", self.root.path());
- command.env("RADROOTS_ACCOUNT_SECRET_BACKEND", "encrypted_file");
- command.env("RADROOTS_ACCOUNT_SECRET_FALLBACK", "none");
+ command.env("RADROOTS_CLI_ACCOUNT_SECRET_BACKEND", "encrypted_file");
+ command.env("RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK", "none");
}
}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -984,7 +984,7 @@ fn config_get_radrootsd_with_bridge_auth_still_reports_deferred_publish_mode() {
let mut command = sandbox.command();
command
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args(["--format", "json", "config", "get"]);
let output = command.output().expect("run config get");
let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
@@ -1034,7 +1034,7 @@ signer_session_ref = "session_ready"
let mut command = sandbox.command();
command
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args(["--format", "json", "config", "get"]);
let output = command.output().expect("run config get");
let value: Value = serde_json::from_slice(&output.stdout).expect("json output");
@@ -1146,7 +1146,7 @@ fn config_get_marks_relay_publish_ready_with_secret_backed_local_account() {
fn config_get_marks_relay_publish_unavailable_with_deferred_signer_mode() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
- sandbox.write_app_config("[signer]\nmode = \"myc\"\n");
+ sandbox.write_app_config("[signer]\nbackend = \"myc\"\n");
let value = sandbox.json_success(&[
"--format",
@@ -1207,7 +1207,7 @@ fn health_surfaces_publish_state_under_deferred_signer_mode() {
let sandbox = RadrootsCliSandbox::new();
let missing_myc = sandbox.root().join("bin/missing-myc");
sandbox.write_app_config(&format!(
- "[publish]\nmode = \"radrootsd\"\n\n[signer]\nmode = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
+ "[publish]\nmode = \"radrootsd\"\n\n[signer]\nbackend = \"myc\"\n\n[myc]\nexecutable = \"{}\"\n",
toml_string(missing_myc.display().to_string().as_str())
));
@@ -1400,7 +1400,7 @@ signer_session_ref = "session_test"
);
let output = sandbox
.command()
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1457,8 +1457,8 @@ fn radrootsd_listing_publish_fails_closed_without_bridge_or_relay_side_effects()
let output = sandbox
.command()
- .env("RADROOTS_RPC_URL", "http://127.0.0.1:9")
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_URL", "http://127.0.0.1:9")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1514,8 +1514,8 @@ fn radrootsd_farm_publish_fails_closed_without_bridge_or_relay_side_effects() {
let output = sandbox
.command()
- .env("RADROOTS_RPC_URL", "http://127.0.0.1:9")
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_URL", "http://127.0.0.1:9")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1581,7 +1581,7 @@ fn radrootsd_farm_publish_ignores_signer_session_binding_and_fails_closed() {
let dry_run_output = sandbox
.command()
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args(["--format", "json", "--dry-run", "farm", "publish"])
.output()
.expect("run radrootsd farm publish dry-run");
@@ -1596,7 +1596,7 @@ fn radrootsd_farm_publish_ignores_signer_session_binding_and_fails_closed() {
let live_output = sandbox
.command()
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1646,7 +1646,7 @@ signer_session_ref = "session_test"
let mut command = sandbox.command();
command
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1700,7 +1700,7 @@ signer_session_ref = "session_test"
);
let mut command = sandbox.command();
command
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1734,8 +1734,8 @@ fn radrootsd_listing_publish_does_not_surface_bridge_errors_before_guardrail() {
let mut command = sandbox.command();
command
- .env("RADROOTS_RPC_URL", "http://127.0.0.1:9")
- .env("RADROOTS_RPC_BEARER_TOKEN", "bridge_test")
+ .env("RADROOTS_CLI_RPC_URL", "http://127.0.0.1:9")
+ .env("RADROOTS_CLI_RPC_BEARER_TOKEN", "bridge_test")
.args([
"--format",
"json",
@@ -1782,7 +1782,7 @@ fn radrootsd_listing_publish_fails_closed_before_relay_or_myc_preflight() {
.as_str()
.expect("farm d tag"),
);
- sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n\n[signer]\nmode = \"myc\"\n");
+ sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n\n[signer]\nbackend = \"myc\"\n");
let (output, value) = sandbox.json_output(&[
"--format",