commit 2ccd369dd5073e1e11f46faa78378e8c08eee067
parent ba7c536e00a0775e279eafcdca36d7597b6deb37
Author: triesap <tyson@radroots.org>
Date: Fri, 10 Apr 2026 20:13:11 +0000
runtime: make write-plane bindings authoritative
Diffstat:
7 files changed, 822 insertions(+), 110 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1658,6 +1658,7 @@ dependencies = [
"radroots_protected_store",
"radroots_replica_db",
"radroots_replica_sync",
+ "radroots_runtime_manager",
"radroots_runtime_paths",
"radroots_secret_vault",
"radroots_sql_core",
@@ -1844,6 +1845,16 @@ dependencies = [
]
[[package]]
+name = "radroots_runtime_manager"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "radroots_runtime_paths",
+ "serde",
+ "thiserror 1.0.69",
+ "toml",
+]
+
+[[package]]
name = "radroots_runtime_paths"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -31,6 +31,7 @@ radroots_nostr_signer = { path = "../lib/crates/nostr_signer" }
radroots_protected_store = { path = "../lib/crates/protected_store", features = ["std"] }
radroots_replica_db = { path = "../lib/crates/replica_db" }
radroots_replica_sync = { path = "../lib/crates/replica_sync" }
+radroots_runtime_manager = { path = "../lib/crates/runtime_manager" }
radroots_runtime_paths = { path = "../lib/crates/runtime_paths" }
radroots_secret_vault = { path = "../lib/crates/secret_vault", features = ["std", "os-keyring"] }
radroots_sql_core = { path = "../lib/crates/sql_core", features = ["native"] }
diff --git a/src/runtime/daemon.rs b/src/runtime/daemon.rs
@@ -12,6 +12,7 @@ use crate::domain::runtime::{
RpcStatusView,
};
use crate::runtime::config::RuntimeConfig;
+use crate::runtime::provider;
const RPC_SOURCE: &str = "daemon rpc · durable write plane";
const BRIDGE_SOURCE: &str = "daemon bridge · durable write plane";
@@ -34,6 +35,12 @@ enum RpcAuthMode {
BridgeBearer,
}
+#[derive(Debug, Clone)]
+struct RpcTarget {
+ url: String,
+ bridge_bearer_token: Option<String>,
+}
+
#[derive(Debug, Serialize)]
struct JsonRpcRequest<'a> {
jsonrpc: &'static str,
@@ -380,8 +387,9 @@ pub fn bridge_listing_publish(
idempotency_key: Option<&str>,
signer_session_id: Option<&str>,
) -> Result<BridgeListingPublishResult, DaemonRpcError> {
+ let target = actor_write_target(config)?;
let response: BridgePublishResponseRemote = call(
- config,
+ &target,
"bridge.listing.publish",
Some(serde_json::json!({
"listing": listing,
@@ -410,8 +418,9 @@ pub fn bridge_order_request(
idempotency_key: Option<&str>,
signer_session_id: Option<&str>,
) -> Result<BridgeOrderRequestResult, DaemonRpcError> {
+ let target = actor_write_target(config)?;
let response: BridgePublishResponseRemote = call(
- config,
+ &target,
"bridge.order.request",
Some(serde_json::json!({
"order": order,
@@ -433,11 +442,21 @@ pub fn bridge_order_request(
}
fn bridge_status(config: &RuntimeConfig) -> Result<BridgeStatusRemote, DaemonRpcError> {
- call(config, "bridge.status", None, RpcAuthMode::BridgeBearer)
+ call(
+ &default_target(config),
+ "bridge.status",
+ None,
+ RpcAuthMode::BridgeBearer,
+ )
}
fn bridge_jobs(config: &RuntimeConfig) -> Result<Vec<BridgeJobRemote>, DaemonRpcError> {
- call(config, "bridge.job.list", None, RpcAuthMode::BridgeBearer)
+ call(
+ &default_target(config),
+ "bridge.job.list",
+ None,
+ RpcAuthMode::BridgeBearer,
+ )
}
fn bridge_job_status(
@@ -445,7 +464,7 @@ fn bridge_job_status(
job_id: &str,
) -> Result<BridgeJobRemote, DaemonRpcError> {
call(
- config,
+ &default_target(config),
"bridge.job.status",
Some(serde_json::json!({ "job_id": job_id })),
RpcAuthMode::BridgeBearer,
@@ -453,7 +472,29 @@ fn bridge_job_status(
}
fn nip46_sessions(config: &RuntimeConfig) -> Result<Vec<Nip46SessionRemote>, DaemonRpcError> {
- call(config, "nip46.session.list", None, RpcAuthMode::None)
+ nip46_sessions_with_target(&default_target(config))
+}
+
+fn nip46_sessions_with_target(
+ target: &RpcTarget,
+) -> Result<Vec<Nip46SessionRemote>, DaemonRpcError> {
+ call(target, "nip46.session.list", None, RpcAuthMode::None)
+}
+
+fn actor_write_target(config: &RuntimeConfig) -> Result<RpcTarget, DaemonRpcError> {
+ let resolved =
+ provider::resolve_actor_write_plane_target(config).map_err(DaemonRpcError::Unconfigured)?;
+ Ok(RpcTarget {
+ url: resolved.url,
+ bridge_bearer_token: Some(resolved.bridge_bearer_token),
+ })
+}
+
+fn default_target(config: &RuntimeConfig) -> RpcTarget {
+ RpcTarget {
+ url: config.rpc.url.clone(),
+ bridge_bearer_token: config.rpc.bridge_bearer_token.clone(),
+ }
}
pub fn resolve_signer_session_id(
@@ -463,7 +504,8 @@ pub fn resolve_signer_session_id(
event_kind: u32,
requested_session_id: Option<&str>,
) -> Result<String, DaemonRpcError> {
- let sessions = nip46_sessions(config)?;
+ let target = actor_write_target(config)?;
+ let sessions = nip46_sessions_with_target(&target)?;
if let Some(session_id) = requested_session_id {
let Some(session) = sessions
@@ -540,7 +582,7 @@ fn sign_event_allowed(perms: &[String], kind: u32) -> bool {
}
fn call<T: DeserializeOwned>(
- config: &RuntimeConfig,
+ target: &RpcTarget,
method: &str,
params: Option<Value>,
auth_mode: RpcAuthMode,
@@ -550,7 +592,7 @@ fn call<T: DeserializeOwned>(
.build()
.map_err(|error| DaemonRpcError::InvalidResponse(format!("build rpc client: {error}")))?;
- let mut request = client.post(config.rpc.url.as_str()).json(&JsonRpcRequest {
+ let mut request = client.post(target.url.as_str()).json(&JsonRpcRequest {
jsonrpc: "2.0",
id: 1,
method,
@@ -558,7 +600,7 @@ fn call<T: DeserializeOwned>(
});
if matches!(auth_mode, RpcAuthMode::BridgeBearer) {
- let Some(token) = config.rpc.bridge_bearer_token.as_deref() else {
+ let Some(token) = target.bridge_bearer_token.as_deref() else {
return Err(DaemonRpcError::Unconfigured(
"bridge bearer token is not configured".to_owned(),
));
@@ -569,7 +611,7 @@ fn call<T: DeserializeOwned>(
let response = request.send().map_err(|error| {
DaemonRpcError::External(format!(
"failed to reach daemon rpc at {}: {error}",
- config.rpc.url
+ target.url
))
})?;
let status = response.status();
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -1,6 +1,13 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use radroots_runtime_manager::{ManagedRuntimeInstallState, load_registry};
+use serde::Deserialize;
+use url::Url;
+
use crate::runtime::config::{
- CapabilityBindingInspection, CapabilityBindingInspectionState, RuntimeConfig,
- INFERENCE_HYF_STDIO_CAPABILITY, WORKFLOW_TRADE_CAPABILITY,
+ CapabilityBindingInspection, CapabilityBindingInspectionState, CapabilityBindingTargetKind,
+ INFERENCE_HYF_STDIO_CAPABILITY, RuntimeConfig, WORKFLOW_TRADE_CAPABILITY,
WRITE_PLANE_TRADE_JSONRPC_CAPABILITY,
};
use crate::runtime::hyf;
@@ -52,6 +59,12 @@ pub struct WritePlaneProviderView {
}
#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ResolvedWritePlaneTarget {
+ pub url: String,
+ pub bridge_bearer_token: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkflowProviderView {
pub provider_runtime_id: String,
pub binding_model: String,
@@ -96,18 +109,27 @@ pub struct HyfProviderView {
pub deterministic_available: Option<bool>,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum WritePlaneResolution {
+ Ready {
+ target: ResolvedWritePlaneTarget,
+ view: WritePlaneProviderView,
+ },
+ Unconfigured(WritePlaneProviderView),
+}
+
pub fn resolve_write_plane_provider(config: &RuntimeConfig) -> WritePlaneProviderView {
- let _binding = inspect_binding(config, WRITE_PLANE_TRADE_JSONRPC_CAPABILITY);
- WritePlaneProviderView {
- provider_runtime_id: "radrootsd".to_owned(),
- binding_model: "daemon_backed_jsonrpc".to_owned(),
- state: "configured".to_owned(),
- provenance: ProviderProvenance::DirectConfig.as_str().to_owned(),
- source: "raw rpc config resolves the current write plane".to_owned(),
- target_kind: None,
- target: Some(config.rpc.url.clone()),
- detail: "actor-authored durable writes still resolve through rpc.url until authoritative write-plane binding resolution lands".to_owned(),
- bridge_auth_configured: config.rpc.bridge_bearer_token.is_some(),
+ match resolve_write_plane_resolution(config) {
+ WritePlaneResolution::Ready { view, .. } | WritePlaneResolution::Unconfigured(view) => view,
+ }
+}
+
+pub fn resolve_actor_write_plane_target(
+ config: &RuntimeConfig,
+) -> Result<ResolvedWritePlaneTarget, String> {
+ match resolve_write_plane_resolution(config) {
+ WritePlaneResolution::Ready { target, .. } => Ok(target),
+ WritePlaneResolution::Unconfigured(view) => Err(view.detail),
}
}
@@ -206,6 +228,300 @@ pub fn resolve_capability_providers(config: &RuntimeConfig) -> Vec<ResolvedProvi
]
}
+fn resolve_write_plane_resolution(config: &RuntimeConfig) -> WritePlaneResolution {
+ if let Some(binding) = config.capability_binding(WRITE_PLANE_TRADE_JSONRPC_CAPABILITY) {
+ return resolve_bound_write_plane(config, binding);
+ }
+
+ match resolve_managed_write_plane_instance(config, "local") {
+ Ok(target) => WritePlaneResolution::Ready {
+ view: WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "configured".to_owned(),
+ provenance: ProviderProvenance::ManagedDefault.as_str().to_owned(),
+ source: "managed preferred radrootsd instance".to_owned(),
+ target_kind: Some("managed_instance".to_owned()),
+ target: Some("local".to_owned()),
+ detail: format!(
+ "actor-authored durable writes resolve through managed radrootsd instance `local` at {}",
+ target.url
+ ),
+ bridge_auth_configured: true,
+ },
+ target,
+ },
+ Err(reason) => WritePlaneResolution::Unconfigured(WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "unconfigured".to_owned(),
+ provenance: ProviderProvenance::Unavailable.as_str().to_owned(),
+ source: "no explicit capability binding or managed preferred default".to_owned(),
+ target_kind: None,
+ target: None,
+ detail: reason,
+ bridge_auth_configured: false,
+ }),
+ }
+}
+
+fn resolve_bound_write_plane(
+ config: &RuntimeConfig,
+ binding: &crate::runtime::config::CapabilityBindingConfig,
+) -> WritePlaneResolution {
+ match binding.target_kind {
+ CapabilityBindingTargetKind::ExplicitEndpoint => {
+ let target_url = match validate_write_plane_url(binding.target.as_str()) {
+ Ok(url) => url,
+ Err(reason) => {
+ return WritePlaneResolution::Unconfigured(WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "unconfigured".to_owned(),
+ provenance: ProviderProvenance::ExplicitBinding.as_str().to_owned(),
+ source: binding.source.as_str().to_owned(),
+ target_kind: Some(binding.target_kind.as_str().to_owned()),
+ target: Some(binding.target.clone()),
+ detail: reason,
+ bridge_auth_configured: false,
+ });
+ }
+ };
+ let Some(bridge_bearer_token) = config
+ .rpc
+ .bridge_bearer_token
+ .as_deref()
+ .map(str::trim)
+ .filter(|token| !token.is_empty())
+ .map(ToOwned::to_owned)
+ else {
+ return WritePlaneResolution::Unconfigured(WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "unconfigured".to_owned(),
+ provenance: ProviderProvenance::ExplicitBinding.as_str().to_owned(),
+ source: binding.source.as_str().to_owned(),
+ target_kind: Some(binding.target_kind.as_str().to_owned()),
+ target: Some(binding.target.clone()),
+ detail:
+ "explicit write-plane capability bindings require RADROOTS_RPC_BEARER_TOKEN for actor-authored durable writes"
+ .to_owned(),
+ bridge_auth_configured: false,
+ });
+ };
+ WritePlaneResolution::Ready {
+ view: WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "configured".to_owned(),
+ provenance: ProviderProvenance::ExplicitBinding.as_str().to_owned(),
+ source: binding.source.as_str().to_owned(),
+ target_kind: Some(binding.target_kind.as_str().to_owned()),
+ target: Some(target_url.clone()),
+ detail: format!(
+ "actor-authored durable writes resolve through explicit write-plane endpoint {}",
+ target_url
+ ),
+ bridge_auth_configured: true,
+ },
+ target: ResolvedWritePlaneTarget {
+ url: target_url,
+ bridge_bearer_token,
+ },
+ }
+ }
+ CapabilityBindingTargetKind::ManagedInstance => {
+ match resolve_managed_write_plane_instance(config, binding.target.as_str()) {
+ Ok(target) => WritePlaneResolution::Ready {
+ view: WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "configured".to_owned(),
+ provenance: ProviderProvenance::ManagedDefault.as_str().to_owned(),
+ source: binding.source.as_str().to_owned(),
+ target_kind: Some(binding.target_kind.as_str().to_owned()),
+ target: Some(binding.target.clone()),
+ detail: format!(
+ "actor-authored durable writes resolve through managed radrootsd instance `{}` at {}",
+ binding.target, target.url
+ ),
+ bridge_auth_configured: true,
+ },
+ target,
+ },
+ Err(reason) => WritePlaneResolution::Unconfigured(WritePlaneProviderView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ binding_model: "daemon_backed_jsonrpc".to_owned(),
+ state: "unconfigured".to_owned(),
+ provenance: ProviderProvenance::ManagedDefault.as_str().to_owned(),
+ source: binding.source.as_str().to_owned(),
+ target_kind: Some(binding.target_kind.as_str().to_owned()),
+ target: Some(binding.target.clone()),
+ detail: reason,
+ bridge_auth_configured: false,
+ }),
+ }
+ }
+ }
+}
+
+fn resolve_managed_write_plane_instance(
+ config: &RuntimeConfig,
+ instance_id: &str,
+) -> Result<ResolvedWritePlaneTarget, String> {
+ let registry_path = runtime_manager_registry_path(config)?;
+ let registry = load_registry(®istry_path).map_err(|err| {
+ format!(
+ "load runtime-manager registry {}: {err}",
+ registry_path.display()
+ )
+ })?;
+ let Some(record) = registry
+ .instances
+ .iter()
+ .find(|record| record.runtime_id == "radrootsd" && record.instance_id == instance_id)
+ else {
+ return Err(format!(
+ "actor-authored durable writes require an explicit write-plane capability binding or managed radrootsd instance `{instance_id}` in {}",
+ registry_path.display()
+ ));
+ };
+ if record.install_state != ManagedRuntimeInstallState::Configured {
+ return Err(format!(
+ "managed radrootsd instance `{instance_id}` is not configured in {}",
+ registry_path.display()
+ ));
+ }
+ load_managed_radrootsd_target(record.config_path.as_path(), instance_id)
+}
+
+fn runtime_manager_registry_path(config: &RuntimeConfig) -> Result<PathBuf, String> {
+ let Some(app_dir) = config.paths.app_config_path.parent() else {
+ return Err("resolve cli app config directory for runtime-manager lookup".to_owned());
+ };
+ let Some(apps_dir) = app_dir.parent() else {
+ return Err("resolve cli apps config root for runtime-manager lookup".to_owned());
+ };
+ let Some(config_root) = apps_dir.parent() else {
+ return Err("resolve cli config root for runtime-manager lookup".to_owned());
+ };
+ Ok(config_root.join("shared/runtime-manager/instances.toml"))
+}
+
+#[derive(Debug, Deserialize)]
+struct ManagedRadrootsdSettingsFile {
+ config: ManagedRadrootsdConfigFile,
+}
+
+#[derive(Debug, Deserialize, Default)]
+struct ManagedRadrootsdConfigFile {
+ #[serde(default)]
+ rpc: ManagedRadrootsdRpcConfig,
+ #[serde(default)]
+ rpc_addr: Option<String>,
+ #[serde(default)]
+ bridge: ManagedRadrootsdBridgeConfig,
+}
+
+#[derive(Debug, Deserialize)]
+struct ManagedRadrootsdRpcConfig {
+ #[serde(default = "default_managed_radrootsd_rpc_addr")]
+ addr: String,
+}
+
+impl Default for ManagedRadrootsdRpcConfig {
+ fn default() -> Self {
+ Self {
+ addr: default_managed_radrootsd_rpc_addr(),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize, Default)]
+struct ManagedRadrootsdBridgeConfig {
+ #[serde(default)]
+ enabled: bool,
+ #[serde(default)]
+ bearer_token: Option<String>,
+}
+
+fn default_managed_radrootsd_rpc_addr() -> String {
+ "127.0.0.1:7070".to_owned()
+}
+
+fn load_managed_radrootsd_target(
+ config_path: &Path,
+ instance_id: &str,
+) -> Result<ResolvedWritePlaneTarget, String> {
+ let raw = fs::read_to_string(config_path).map_err(|err| {
+ format!(
+ "read managed radrootsd config for instance `{instance_id}` at {}: {err}",
+ config_path.display()
+ )
+ })?;
+ let settings: ManagedRadrootsdSettingsFile = toml::from_str(raw.as_str()).map_err(|err| {
+ format!(
+ "parse managed radrootsd config for instance `{instance_id}` at {}: {err}",
+ config_path.display()
+ )
+ })?;
+ if !settings.config.bridge.enabled {
+ return Err(format!(
+ "managed radrootsd instance `{instance_id}` has bridge ingress disabled in {}",
+ config_path.display()
+ ));
+ }
+ let Some(bridge_bearer_token) = settings
+ .config
+ .bridge
+ .bearer_token
+ .as_deref()
+ .map(str::trim)
+ .filter(|token| !token.is_empty())
+ .map(ToOwned::to_owned)
+ else {
+ return Err(format!(
+ "managed radrootsd instance `{instance_id}` is missing bridge bearer_token in {}",
+ config_path.display()
+ ));
+ };
+ let rpc_addr = settings
+ .config
+ .rpc_addr
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .unwrap_or(settings.config.rpc.addr.as_str());
+ let url = rpc_addr_to_url(rpc_addr)?;
+ Ok(ResolvedWritePlaneTarget {
+ url,
+ bridge_bearer_token,
+ })
+}
+
+fn rpc_addr_to_url(value: &str) -> Result<String, String> {
+ let trimmed = value.trim();
+ if trimmed.contains("://") {
+ return validate_write_plane_url(trimmed);
+ }
+ validate_write_plane_url(format!("http://{trimmed}").as_str())
+}
+
+fn validate_write_plane_url(value: &str) -> Result<String, String> {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ return Err("write-plane endpoint must not be empty".to_owned());
+ }
+ let parsed = Url::parse(trimmed)
+ .map_err(|err| format!("write-plane endpoint `{trimmed}` is invalid: {err}"))?;
+ if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
+ return Err(format!(
+ "write-plane endpoint must use http or https, got `{trimmed}`"
+ ));
+ }
+ Ok(trimmed.to_owned())
+}
+
fn inspect_binding(config: &RuntimeConfig, capability_id: &str) -> CapabilityBindingInspection {
config
.inspect_capability_bindings()
@@ -256,7 +572,10 @@ fn hyf_executable(
if binding.state == CapabilityBindingInspectionState::Configured
&& binding.target_kind.as_deref() == Some("explicit_endpoint")
{
- return binding.target.clone().unwrap_or_else(|| status.executable.clone());
+ return binding
+ .target
+ .clone()
+ .unwrap_or_else(|| status.executable.clone());
}
if !config.hyf.enabled {
return status.executable.clone();
@@ -266,14 +585,16 @@ fn hyf_executable(
#[cfg(test)]
mod tests {
- use std::path::PathBuf;
+ use std::fs;
+ use std::path::{Path, PathBuf};
use radroots_runtime_paths::RadrootsMigrationReport;
use radroots_secret_vault::RadrootsSecretBackend;
+ use tempfile::tempdir;
use super::{
- ProviderProvenance, resolve_capability_providers, resolve_hyf_provider,
- resolve_workflow_provider, resolve_write_plane_provider,
+ ProviderProvenance, resolve_actor_write_plane_target, resolve_capability_providers,
+ resolve_hyf_provider, resolve_workflow_provider, resolve_write_plane_provider,
};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
@@ -302,8 +623,8 @@ mod tests {
app_namespace: "apps/cli".into(),
shared_accounts_namespace: "shared/accounts".into(),
shared_identities_namespace: "shared/identities".into(),
- app_config_path: PathBuf::from("/tmp/config.toml"),
- workspace_config_path: PathBuf::from("/tmp/workspace-config.toml"),
+ app_config_path: PathBuf::from("/tmp/config/apps/cli/config.toml"),
+ workspace_config_path: PathBuf::from("/tmp/workspace/.radroots/config.toml"),
app_data_root: PathBuf::from("/tmp/data"),
app_logs_root: PathBuf::from("/tmp/logs"),
shared_accounts_data_root: PathBuf::from("/tmp/shared/accounts"),
@@ -365,10 +686,11 @@ mod tests {
}
#[test]
- fn write_plane_uses_direct_config_provenance() {
+ fn write_plane_requires_authoritative_binding_or_managed_default() {
let view = resolve_write_plane_provider(&sample_config(Vec::new(), false));
- assert_eq!(view.provenance, ProviderProvenance::DirectConfig.as_str());
- assert_eq!(view.target.as_deref(), Some("http://127.0.0.1:7070"));
+ assert_eq!(view.state, "unconfigured");
+ assert_eq!(view.provenance, ProviderProvenance::Unavailable.as_str());
+ assert!(view.target.is_none());
}
#[test]
@@ -384,11 +706,90 @@ mod tests {
signer_session_ref: None,
};
let view = resolve_workflow_provider(&sample_config(vec![binding], false));
- assert_eq!(view.provenance, ProviderProvenance::ExplicitBinding.as_str());
+ assert_eq!(
+ view.provenance,
+ ProviderProvenance::ExplicitBinding.as_str()
+ );
assert_eq!(view.target_kind.as_deref(), Some("explicit_endpoint"));
}
#[test]
+ fn explicit_write_plane_binding_requires_bridge_bearer_auth() {
+ let binding = CapabilityBindingConfig {
+ capability_id: "write_plane.trade_jsonrpc".into(),
+ provider_runtime_id: "radrootsd".into(),
+ binding_model: "daemon_backed_jsonrpc".into(),
+ source: CapabilityBindingSource::WorkspaceConfig,
+ target_kind: CapabilityBindingTargetKind::ExplicitEndpoint,
+ target: "https://rpc.workspace.test".into(),
+ managed_account_ref: None,
+ signer_session_ref: None,
+ };
+ let view = resolve_write_plane_provider(&sample_config(vec![binding], false));
+ assert_eq!(view.state, "unconfigured");
+ assert_eq!(
+ view.provenance,
+ ProviderProvenance::ExplicitBinding.as_str()
+ );
+ assert_eq!(view.target.as_deref(), Some("https://rpc.workspace.test"));
+ }
+
+ #[test]
+ fn managed_default_write_plane_uses_runtime_manager_registry() {
+ let dir = tempdir().expect("tempdir");
+ let config_dir = dir.path().join("config");
+ let app_config_path = config_dir.join("apps/cli/config.toml");
+ fs::create_dir_all(app_config_path.parent().expect("app config parent"))
+ .expect("create app config dir");
+ fs::write(&app_config_path, "").expect("write app config");
+
+ let registry_path = config_dir.join("shared/runtime-manager/instances.toml");
+ fs::create_dir_all(registry_path.parent().expect("registry parent"))
+ .expect("create registry parent");
+ let managed_config_path = dir.path().join("radrootsd-config.toml");
+ write_managed_radrootsd_config(
+ managed_config_path.as_path(),
+ "127.0.0.1:7444",
+ "managed-bridge-token",
+ );
+ fs::write(
+ ®istry_path,
+ format!(
+ r#"schema = "radroots_runtime-instance-registry"
+schema_version = 1
+
+[[instances]]
+runtime_id = "radrootsd"
+instance_id = "local"
+management_mode = "interactive_user_managed"
+install_state = "configured"
+binary_path = "/tmp/radrootsd"
+config_path = "{}"
+logs_path = "/tmp/logs"
+run_path = "/tmp/run"
+installed_version = "0.1.0"
+"#,
+ managed_config_path.display()
+ ),
+ )
+ .expect("write registry");
+
+ let mut config = sample_config(Vec::new(), false);
+ config.paths.app_config_path = app_config_path;
+
+ let view = resolve_write_plane_provider(&config);
+ assert_eq!(view.state, "configured");
+ assert_eq!(view.provenance, ProviderProvenance::ManagedDefault.as_str());
+ assert_eq!(view.target_kind.as_deref(), Some("managed_instance"));
+ assert_eq!(view.target.as_deref(), Some("local"));
+
+ let target =
+ resolve_actor_write_plane_target(&config).expect("resolve actor write plane target");
+ assert_eq!(target.url, "http://127.0.0.1:7444");
+ assert_eq!(target.bridge_bearer_token, "managed-bridge-token");
+ }
+
+ #[test]
fn hyf_uses_direct_config_when_enabled_without_binding() {
let view = resolve_hyf_provider(&sample_config(Vec::new(), true));
assert_eq!(view.provenance, ProviderProvenance::DirectConfig.as_str());
@@ -410,7 +811,10 @@ mod tests {
};
let view = resolve_hyf_provider(&sample_config(vec![binding], false));
assert_eq!(view.state, "disabled");
- assert_eq!(view.provenance, ProviderProvenance::ExplicitBinding.as_str());
+ assert_eq!(
+ view.provenance,
+ ProviderProvenance::ExplicitBinding.as_str()
+ );
assert_eq!(view.source, "user config [[capability_binding]]");
assert_eq!(view.target_kind.as_deref(), Some("explicit_endpoint"));
assert_eq!(view.target.as_deref(), Some("bin/hyfd-user"));
@@ -425,4 +829,25 @@ mod tests {
assert_eq!(providers[1].capability_id, "workflow.trade");
assert_eq!(providers[2].capability_id, "inference.hyf_stdio");
}
+
+ fn write_managed_radrootsd_config(path: &Path, rpc_addr: &str, bearer_token: &str) {
+ fs::write(
+ path,
+ format!(
+ r#"[metadata]
+name = "managed-radrootsd"
+
+[config]
+
+[config.rpc]
+addr = "{rpc_addr}"
+
+[config.bridge]
+enabled = true
+bearer_token = "{bearer_token}"
+"#
+ ),
+ )
+ .expect("write managed radrootsd config");
+ }
}
diff --git a/tests/listing.rs b/tests/listing.rs
@@ -62,6 +62,26 @@ fn write_workspace_config(workdir: &Path, contents: &str) {
fs::write(config_dir.join("config.toml"), contents).expect("write workspace config");
}
+fn workspace_config_with_write_plane(extra: &str, url: &str) -> String {
+ let mut rendered = String::new();
+ if !extra.trim().is_empty() {
+ rendered.push_str(extra.trim());
+ rendered.push_str("\n\n");
+ }
+ rendered.push_str(
+ format!(
+ r#"[[capability_binding]]
+capability = "write_plane.trade_jsonrpc"
+provider = "radrootsd"
+target_kind = "explicit_endpoint"
+target = "{url}"
+"#
+ )
+ .as_str(),
+ );
+ rendered
+}
+
fn write_fake_myc(dir: &Path, script: &str) -> std::path::PathBuf {
let path = dir.join("fake-myc");
fs::write(&path, script).expect("write fake myc");
@@ -396,9 +416,12 @@ fn listing_publish_and_update_use_durable_bridge_publish() {
other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -437,7 +460,6 @@ fn listing_publish_and_update_use_durable_bridge_publish() {
);
let update_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -545,9 +567,12 @@ fn listing_archive_and_dry_run_are_truthful() {
other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let archive_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -665,20 +690,6 @@ fn listing_publish_uses_myc_binding_before_resolving_daemon_signer_session() {
)
.expect("write listing draft");
- write_workspace_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{account_id}"
-"#
- )
- .as_str(),
- );
let myc = write_fake_myc(
dir.path(),
successful_status_script(
@@ -718,9 +729,26 @@ managed_account_ref = "{account_id}"
other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane(
+ format!(
+ r#"
+[[capability_binding]]
+capability = "signer.remote_nip46"
+provider = "myc"
+target_kind = "managed_instance"
+target = "default"
+managed_account_ref = "{account_id}"
+"#
+ )
+ .as_str(),
+ server.url().as_str(),
+ )
+ .as_str(),
+ );
let output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -816,20 +844,6 @@ fn listing_publish_rejects_myc_binding_that_resolves_the_wrong_actor() {
)
.expect("write listing draft");
- write_workspace_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{mismatch_account_id}"
-"#
- )
- .as_str(),
- );
let myc = write_fake_myc(
dir.path(),
successful_status_script(
@@ -849,9 +863,26 @@ managed_account_ref = "{mismatch_account_id}"
recorded.lock().expect("recorded").push(body.clone());
MockRpcResponse::rpc_error(-32601, "daemon write path should not be reached")
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane(
+ format!(
+ r#"
+[[capability_binding]]
+capability = "signer.remote_nip46"
+provider = "myc"
+target_kind = "managed_instance"
+target = "default"
+managed_account_ref = "{mismatch_account_id}"
+"#
+ )
+ .as_str(),
+ server.url().as_str(),
+ )
+ .as_str(),
+ );
let output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -942,9 +973,12 @@ fn listing_publish_without_matching_signer_session_exits_unconfigured() {
other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -1036,9 +1070,12 @@ fn listing_publish_rejects_requested_session_that_mismatches_seller_pubkey() {
other => MockRpcResponse::rpc_error(-32601, &format!("unexpected method: {other}")),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let publish_output = cli_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
.args([
"--json",
@@ -1066,6 +1103,88 @@ fn listing_publish_rejects_requested_session_that_mismatches_seller_pubkey() {
assert_eq!(recorded[0]["method"], "nip46.session.list");
}
+#[test]
+fn listing_publish_requires_authoritative_write_plane_binding() {
+ let _guard = listing_test_guard();
+ let dir = tempdir().expect("tempdir");
+ let init = cli_command_in(dir.path())
+ .args(["local", "init"])
+ .output()
+ .expect("run local init");
+ assert!(init.status.success());
+
+ let account_output = cli_command_in(dir.path())
+ .args(["--json", "account", "new"])
+ .output()
+ .expect("run account new");
+ assert!(account_output.status.success());
+ let account_json: Value =
+ serde_json::from_slice(account_output.stdout.as_slice()).expect("account json");
+ let seller_pubkey = account_json["public_identity"]["public_key_hex"]
+ .as_str()
+ .expect("seller pubkey")
+ .to_owned();
+ seed_farm(
+ dir.path(),
+ seller_pubkey.as_str(),
+ "AAAAAAAAAAAAAAAAAAAAAw",
+ "La Huerta",
+ );
+
+ let draft_path = dir.path().join("missing-write-binding.toml");
+ fs::write(
+ &draft_path,
+ valid_listing_draft(
+ "AAAAAAAAAAAAAAAAAAAAAg",
+ "",
+ "",
+ "eggs",
+ "Pasture eggs",
+ "Protein",
+ "Fresh pasture-raised eggs collected daily.",
+ "12",
+ "each",
+ "4.50",
+ "USD",
+ "1",
+ "each",
+ "18",
+ "pickup",
+ "La Huerta del Sur",
+ ),
+ )
+ .expect("write listing draft");
+
+ let requests = Arc::new(Mutex::new(Vec::<Value>::new()));
+ let recorded = Arc::clone(&requests);
+ let server = MockRpcServer::start(move |body, _auth_header| {
+ recorded.lock().expect("recorded").push(body);
+ MockRpcResponse::rpc_error(-32601, "daemon write path should not be reached")
+ });
+
+ let publish_output = cli_command_in(dir.path())
+ .env("RADROOTS_RPC_URL", server.url())
+ .env("RADROOTS_RPC_BEARER_TOKEN", "bridge-secret")
+ .args([
+ "--json",
+ "listing",
+ "publish",
+ draft_path.to_str().expect("draft path"),
+ ])
+ .output()
+ .expect("run listing publish");
+ assert_eq!(publish_output.status.code(), Some(3));
+ let publish_json: Value =
+ serde_json::from_slice(publish_output.stdout.as_slice()).expect("publish json");
+ assert_eq!(publish_json["state"], "unconfigured");
+ assert!(
+ publish_json["reason"].as_str().expect("reason").contains(
+ "explicit write-plane capability binding or managed radrootsd instance `local`"
+ )
+ );
+ assert!(requests.lock().expect("requests").is_empty());
+}
+
fn seed_farm(workdir: &Path, pubkey: &str, d_tag: &str, name: &str) {
let replica_db = data_root(workdir).join("apps/cli/replica/replica.sqlite");
let executor = SqliteExecutor::open(&replica_db).expect("open replica db");
diff --git a/tests/order.rs b/tests/order.rs
@@ -61,6 +61,26 @@ fn write_workspace_config(workdir: &Path, contents: &str) {
fs::write(config_dir.join("config.toml"), contents).expect("write workspace config");
}
+fn workspace_config_with_write_plane(extra: &str, url: &str) -> String {
+ let mut rendered = String::new();
+ if !extra.trim().is_empty() {
+ rendered.push_str(extra.trim());
+ rendered.push_str("\n\n");
+ }
+ rendered.push_str(
+ format!(
+ r#"[[capability_binding]]
+capability = "write_plane.trade_jsonrpc"
+provider = "radrootsd"
+target_kind = "explicit_endpoint"
+target = "{url}"
+"#
+ )
+ .as_str(),
+ );
+ rendered
+}
+
fn write_fake_myc(dir: &Path, script: &str) -> std::path::PathBuf {
let path = dir.join("fake-myc");
fs::write(&path, script).expect("write fake myc");
@@ -566,9 +586,12 @@ fn order_submit_persists_submission_metadata_and_reports_job() {
other => panic!("unexpected mock rpc method {other}"),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
.args([
"--json",
@@ -728,20 +751,6 @@ fn order_submit_uses_myc_binding_before_resolving_daemon_signer_session() {
.expect("buyer pubkey")
.to_owned();
- write_workspace_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{account_id}"
-"#
- )
- .as_str(),
- );
let myc = write_fake_myc(
dir.path(),
successful_status_script(
@@ -796,9 +805,26 @@ managed_account_ref = "{account_id}"
other => panic!("unexpected mock rpc method {other}"),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane(
+ format!(
+ r#"
+[[capability_binding]]
+capability = "signer.remote_nip46"
+provider = "myc"
+target_kind = "managed_instance"
+target = "default"
+managed_account_ref = "{account_id}"
+"#
+ )
+ .as_str(),
+ server.url().as_str(),
+ )
+ .as_str(),
+ );
let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
.args([
"--json",
@@ -877,20 +903,6 @@ fn order_submit_rejects_myc_binding_that_resolves_the_wrong_actor() {
.expect("mismatch account id");
let mismatch_public_identity = mismatch_account_json["public_identity"].clone();
- write_workspace_config(
- dir.path(),
- format!(
- r#"
-[[capability_binding]]
-capability = "signer.remote_nip46"
-provider = "myc"
-target_kind = "managed_instance"
-target = "default"
-managed_account_ref = "{mismatch_account_id}"
-"#
- )
- .as_str(),
- );
let myc = write_fake_myc(
dir.path(),
successful_status_script(
@@ -917,9 +929,26 @@ managed_account_ref = "{mismatch_account_id}"
});
panic!("daemon write path should not be reached");
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane(
+ format!(
+ r#"
+[[capability_binding]]
+capability = "signer.remote_nip46"
+provider = "myc"
+target_kind = "managed_instance"
+target = "default"
+managed_account_ref = "{mismatch_account_id}"
+"#
+ )
+ .as_str(),
+ server.url().as_str(),
+ )
+ .as_str(),
+ );
let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
.args([
"--json",
@@ -1009,9 +1038,12 @@ fn order_submit_without_unique_matching_signer_session_exits_unconfigured() {
other => panic!("unexpected mock rpc method {other}"),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
.args(["--json", "order", "submit", order_id])
.output()
@@ -1082,9 +1114,12 @@ fn order_submit_rejects_requested_session_that_mismatches_buyer_pubkey() {
other => panic!("unexpected mock rpc method {other}"),
}
});
+ write_workspace_config(
+ dir.path(),
+ workspace_config_with_write_plane("", server.url().as_str()).as_str(),
+ );
let submit_output = order_command_in(dir.path())
- .env("RADROOTS_RPC_URL", server.url())
.env("RADROOTS_RPC_BEARER_TOKEN", "test-token")
.args([
"--json",
diff --git a/tests/runtime_show.rs b/tests/runtime_show.rs
@@ -30,6 +30,10 @@ fn config_root(workdir: &Path) -> std::path::PathBuf {
}
}
+fn runtime_manager_registry_path(workdir: &Path) -> std::path::PathBuf {
+ config_root(workdir).join("shared/runtime-manager/instances.toml")
+}
+
fn data_root(workdir: &Path) -> std::path::PathBuf {
if cfg!(windows) {
localappdata_root(workdir).join("data")
@@ -280,14 +284,17 @@ fn config_show_json_reports_default_bootstrap_state() {
);
assert_eq!(json["myc"]["executable"], "myc");
assert_eq!(json["write_plane"]["provider_runtime_id"], "radrootsd");
- assert_eq!(json["write_plane"]["binding_model"], "daemon_backed_jsonrpc");
- assert_eq!(json["write_plane"]["state"], "configured");
- assert_eq!(json["write_plane"]["provenance"], "direct_config");
+ assert_eq!(
+ json["write_plane"]["binding_model"],
+ "daemon_backed_jsonrpc"
+ );
+ assert_eq!(json["write_plane"]["state"], "unconfigured");
+ assert_eq!(json["write_plane"]["provenance"], "unavailable");
assert_eq!(
json["write_plane"]["source"],
- "raw rpc config resolves the current write plane"
+ "no explicit capability binding or managed preferred default"
);
- assert_eq!(json["write_plane"]["target"], "http://127.0.0.1:7070");
+ assert!(json["write_plane"]["target"].is_null());
assert_eq!(json["write_plane"]["bridge_auth_configured"], false);
assert_eq!(json["workflow"]["provider_runtime_id"], "rhi");
assert_eq!(json["workflow"]["binding_model"], "out_of_process_worker");
@@ -690,8 +697,18 @@ target = "bin/hyfd-user"
.as_str()
.is_some_and(|detail| detail.contains("do not imply"))
);
- assert_eq!(json["write_plane"]["provenance"], "direct_config");
- assert_eq!(json["write_plane"]["target"], "http://127.0.0.1:7070");
+ assert_eq!(json["write_plane"]["state"], "unconfigured");
+ assert_eq!(json["write_plane"]["provenance"], "explicit_binding");
+ assert_eq!(
+ json["write_plane"]["source"],
+ "workspace config [[capability_binding]]"
+ );
+ assert_eq!(json["write_plane"]["target_kind"], "explicit_endpoint");
+ assert_eq!(
+ json["write_plane"]["target"],
+ "https://rpc.workspace.test/jsonrpc"
+ );
+ assert_eq!(json["write_plane"]["bridge_auth_configured"], false);
assert_eq!(json["hyf_provider"]["provider_runtime_id"], "hyf");
assert_eq!(json["hyf_provider"]["provenance"], "explicit_binding");
assert_eq!(json["hyf_provider"]["target_kind"], "explicit_endpoint");
@@ -706,6 +723,68 @@ target = "bin/hyfd-user"
}
#[test]
+fn config_show_uses_managed_default_write_plane_when_local_instance_exists() {
+ let dir = tempdir().expect("tempdir");
+ let registry_path = runtime_manager_registry_path(dir.path());
+ fs::create_dir_all(registry_path.parent().expect("registry parent")).expect("registry dir");
+ let managed_config_path = dir.path().join("managed-radrootsd.toml");
+ fs::write(
+ &managed_config_path,
+ r#"[metadata]
+name = "managed-radrootsd"
+
+[config]
+
+[config.rpc]
+addr = "127.0.0.1:7444"
+
+[config.bridge]
+enabled = true
+bearer_token = "managed-bridge-token"
+"#,
+ )
+ .expect("write managed config");
+ fs::write(
+ ®istry_path,
+ format!(
+ r#"schema = "radroots_runtime-instance-registry"
+schema_version = 1
+
+[[instances]]
+runtime_id = "radrootsd"
+instance_id = "local"
+management_mode = "interactive_user_managed"
+install_state = "configured"
+binary_path = "/tmp/radrootsd"
+config_path = "{}"
+logs_path = "/tmp/radrootsd/logs"
+run_path = "/tmp/radrootsd/run"
+installed_version = "0.1.0"
+"#,
+ managed_config_path.display()
+ ),
+ )
+ .expect("write managed registry");
+
+ let output = runtime_show_command_in(dir.path())
+ .args(["--json", "config", "show"])
+ .output()
+ .expect("run config show");
+
+ assert!(output.status.success());
+ let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
+ assert_eq!(json["write_plane"]["state"], "configured");
+ assert_eq!(json["write_plane"]["provenance"], "managed_default");
+ assert_eq!(
+ json["write_plane"]["source"],
+ "managed preferred radrootsd instance"
+ );
+ assert_eq!(json["write_plane"]["target_kind"], "managed_instance");
+ assert_eq!(json["write_plane"]["target"], "local");
+ assert_eq!(json["write_plane"]["bridge_auth_configured"], true);
+}
+
+#[test]
fn config_show_rejects_ndjson_for_singular_output() {
let dir = tempdir().expect("tempdir");
let output = runtime_show_command_in(dir.path())