commit 237e62d1e14374ec0d300b9c2495401509139917
parent 649e1b21038ce4a069996dc33b3981cd0afe3147
Author: triesap <tyson@radroots.org>
Date: Thu, 7 May 2026 02:53:03 +0000
cli: surface publish runtime state
- add resolved publish state to config output
- expose publish readiness through health commands
- report write plane posture from runtime config
- cover publish observability under deferred signer mode
Diffstat:
6 files changed, 221 insertions(+), 9 deletions(-)
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -48,6 +48,7 @@ pub struct ConfigShowView {
pub logging: LoggingRuntimeView,
pub account: AccountRuntimeView,
pub signer: SignerRuntimeView,
+ pub publish: PublishRuntimeView,
pub relay: RelayRuntimeView,
pub local: LocalRuntimeView,
pub myc: MycRuntimeView,
@@ -298,6 +299,36 @@ pub struct RelayRuntimeView {
}
#[derive(Debug, Clone, Serialize)]
+pub struct PublishRuntimeView {
+ pub mode: String,
+ pub source: String,
+ pub transport_family: String,
+ pub state: String,
+ pub executable: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option<String>,
+ pub signed_write_required: bool,
+ pub relay: PublishRelayRuntimeView,
+ pub provider: PublishProviderRuntimeView,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct PublishRelayRuntimeView {
+ pub ready: bool,
+ pub count: usize,
+ pub source: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct PublishProviderRuntimeView {
+ pub provider_runtime_id: String,
+ pub state: String,
+ pub source: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize)]
pub struct LocalRuntimeView {
pub root: String,
pub replica_db_path: String,
diff --git a/src/operation_core.rs b/src/operation_core.rs
@@ -3,7 +3,10 @@ use std::path::PathBuf;
use serde::Serialize;
use serde_json::{Value, json};
-use crate::domain::runtime::{CommandDisposition, LocalBackupView};
+use crate::domain::runtime::{
+ CommandDisposition, LocalBackupView, PublishProviderRuntimeView, PublishRelayRuntimeView,
+ PublishRuntimeView,
+};
use crate::operation_adapter::{
AccountAttachSecretRequest, AccountAttachSecretResult, AccountCreateRequest,
AccountCreateResult, AccountGetRequest, AccountGetResult, AccountImportRequest,
@@ -26,7 +29,7 @@ use crate::runtime::accounts::{
resolve_account_resolution, resolve_account_selector, secret_backend_status, select_account,
snapshot, unresolved_account_reason,
};
-use crate::runtime::config::RuntimeConfig;
+use crate::runtime::config::{PublishMode, RuntimeConfig};
use crate::runtime::logging::LoggingState;
use crate::runtime_args::LocalExportFormatArg;
@@ -97,10 +100,12 @@ impl OperationService<HealthStatusGetRequest> for CoreOperationService<'_> {
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
let store = map_runtime(crate::runtime::local::status(self.config))?;
let account = map_runtime(resolve_account_resolution(self.config))?;
+ let publish = publish_runtime_view(self.config, false);
json_operation_result::<HealthStatusGetResult>(json!({
"state": if store.state == "ready" { "ready" } else { "needs_attention" },
"store": store,
"account_resolution": account_resolution_view(&account),
+ "publish": publish,
"logging": {
"initialized": self.logging.initialized,
"current_file": self.logging.current_file.as_ref().map(|path| path.display().to_string()),
@@ -123,6 +128,7 @@ impl OperationService<HealthCheckRunRequest> for CoreOperationService<'_> {
} else {
Some(map_runtime(unresolved_account_reason(self.config))?)
};
+ let publish = publish_runtime_view(self.config, false);
json_operation_result::<HealthCheckRunResult>(json!({
"state": if store.state == "ready" && account.resolved_account.is_some() { "ready" } else { "needs_attention" },
"checks": {
@@ -138,6 +144,12 @@ impl OperationService<HealthCheckRunRequest> for CoreOperationService<'_> {
"state": if account.resolved_account.is_some() { "ready" } else { "unconfigured" },
"reason": account_reason,
},
+ "publish": {
+ "state": publish.state,
+ "mode": publish.mode,
+ "executable": publish.executable,
+ "reason": publish.reason,
+ },
},
}))
}
@@ -150,6 +162,7 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
&self,
_request: OperationRequest<ConfigGetRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
+ let write_plane = crate::runtime::provider::resolve_write_plane_provider(self.config);
json_operation_result::<ConfigGetResult>(json!({
"output": {
"format": self.config.output.format.as_str(),
@@ -174,11 +187,38 @@ impl OperationService<ConfigGetRequest> for CoreOperationService<'_> {
"store_path": self.config.account.store_path.display().to_string(),
"secrets_dir": self.config.account.secrets_dir.display().to_string(),
},
+ "signer": {
+ "mode": self.config.signer.backend.as_str(),
+ },
+ "publish": publish_runtime_view(self.config, false),
"relay": {
"count": self.config.relay.urls.len(),
"urls": self.config.relay.urls,
"source": self.config.relay.source.as_str(),
},
+ "myc": {
+ "executable": self.config.myc.executable.display().to_string(),
+ "status_timeout_ms": self.config.myc.status_timeout_ms,
+ },
+ "hyf": {
+ "enabled": self.config.hyf.enabled,
+ "executable": self.config.hyf.executable.display().to_string(),
+ },
+ "rpc": {
+ "url": self.config.rpc.url,
+ "bridge_auth_configured": self.config.rpc.bridge_bearer_token.is_some(),
+ },
+ "write_plane": {
+ "provider_runtime_id": write_plane.provider_runtime_id,
+ "binding_model": write_plane.binding_model,
+ "state": write_plane.state,
+ "provenance": write_plane.provenance,
+ "source": write_plane.source,
+ "target_kind": write_plane.target_kind,
+ "target": write_plane.target,
+ "detail": write_plane.detail,
+ "bridge_auth_configured": self.config.rpc.bridge_bearer_token.is_some(),
+ },
"local": {
"root": self.config.local.root.display().to_string(),
"replica_db_path": self.config.local.replica_db_path.display().to_string(),
@@ -644,6 +684,70 @@ fn selected_config(config: &RuntimeConfig, selector: String) -> RuntimeConfig {
config
}
+fn publish_runtime_view(config: &RuntimeConfig, signed_write_required: bool) -> PublishRuntimeView {
+ let relay_ready = !config.relay.urls.is_empty();
+ let source = config.publish.source.as_str().to_owned();
+ let relay = PublishRelayRuntimeView {
+ ready: relay_ready,
+ count: config.relay.urls.len(),
+ source: config.relay.source.as_str().to_owned(),
+ };
+
+ match config.publish.mode {
+ PublishMode::NostrRelay => {
+ let reason = (!relay_ready).then(|| {
+ "nostr_relay publish mode requires at least one configured relay for writes"
+ .to_owned()
+ });
+ PublishRuntimeView {
+ mode: config.publish.mode.as_str().to_owned(),
+ source,
+ transport_family: config.publish.mode.transport_family().to_owned(),
+ state: if relay_ready {
+ "ready".to_owned()
+ } else {
+ "unconfigured".to_owned()
+ },
+ executable: relay_ready,
+ reason: reason.clone(),
+ signed_write_required,
+ relay,
+ provider: PublishProviderRuntimeView {
+ provider_runtime_id: "nostr_relay".to_owned(),
+ state: if relay_ready {
+ "ready".to_owned()
+ } else {
+ "unconfigured".to_owned()
+ },
+ source: config.relay.source.as_str().to_owned(),
+ reason,
+ },
+ }
+ }
+ PublishMode::Radrootsd => PublishRuntimeView {
+ mode: config.publish.mode.as_str().to_owned(),
+ source,
+ transport_family: config.publish.mode.transport_family().to_owned(),
+ state: "unavailable".to_owned(),
+ executable: false,
+ reason: Some(
+ "radrootsd publish mode is configured but the radrootsd publish transport is not implemented"
+ .to_owned(),
+ ),
+ signed_write_required,
+ relay,
+ provider: PublishProviderRuntimeView {
+ provider_runtime_id: "radrootsd".to_owned(),
+ state: "unavailable".to_owned(),
+ source: "publish mode · local first".to_owned(),
+ reason: Some(
+ "radrootsd publish transport is reserved for a future implementation".to_owned(),
+ ),
+ },
+ },
+ }
+}
+
fn required_string<P>(
request: &OperationRequest<P>,
key: &str,
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -195,6 +195,13 @@ impl PublishMode {
Self::Radrootsd => "radrootsd",
}
}
+
+ pub fn transport_family(self) -> &'static str {
+ match self {
+ Self::NostrRelay => "nostr_relay",
+ Self::Radrootsd => "radrootsd",
+ }
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -206,6 +213,18 @@ pub enum PublishModeSource {
Defaults,
}
+impl PublishModeSource {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Flags => "cli flags · local first",
+ Self::Environment => "environment · local first",
+ Self::UserConfig => "user config · local first",
+ Self::WorkspaceConfig => "workspace config · local first",
+ Self::Defaults => "defaults · local first",
+ }
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishConfig {
pub mode: PublishMode,
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -11,7 +11,6 @@ pub mod logging;
pub mod network;
pub mod order;
pub mod paths;
-#[cfg(test)]
pub mod provider;
pub mod signer;
pub mod sync;
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -10,10 +10,11 @@ use crate::runtime::hyf;
const WRITE_PLANE_UNAVAILABLE_DETAIL: &str = "legacy write-plane provider is unavailable; use seller publish commands with configured direct relays";
-#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderProvenance {
+ #[cfg(test)]
ExplicitBinding,
+ #[cfg(test)]
ManagedDefault,
#[cfg(test)]
DirectConfig,
@@ -22,11 +23,12 @@ pub enum ProviderProvenance {
Unavailable,
}
-#[cfg(test)]
impl ProviderProvenance {
pub fn as_str(self) -> &'static str {
match self {
+ #[cfg(test)]
Self::ExplicitBinding => "explicit_binding",
+ #[cfg(test)]
Self::ManagedDefault => "managed_default",
#[cfg(test)]
Self::DirectConfig => "direct_config",
@@ -50,7 +52,6 @@ pub struct ResolvedProviderView {
pub target: Option<String>,
}
-#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritePlaneProviderView {
pub provider_runtime_id: String,
@@ -64,6 +65,7 @@ pub struct WritePlaneProviderView {
pub bridge_auth_configured: bool,
}
+#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedWritePlaneTarget {
pub url: String,
@@ -86,12 +88,12 @@ pub struct HyfProviderView {
pub deterministic_available: Option<bool>,
}
-#[cfg(test)]
pub fn resolve_write_plane_provider(config: &RuntimeConfig) -> WritePlaneProviderView {
let _ = config;
unavailable_write_plane_view()
}
+#[cfg(test)]
pub fn resolve_actor_write_plane_target(
config: &RuntimeConfig,
) -> Result<ResolvedWritePlaneTarget, String> {
@@ -153,7 +155,6 @@ pub fn resolve_capability_providers(config: &RuntimeConfig) -> Vec<ResolvedProvi
}]
}
-#[cfg(test)]
fn unavailable_write_plane_view() -> WritePlaneProviderView {
WritePlaneProviderView {
provider_runtime_id: "nostr_relay".to_owned(),
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -8,7 +8,7 @@ use serde_json::Value;
use support::{
RadrootsCliSandbox, assert_no_daemon_runtime_reference, assert_no_removed_command_reference,
create_listing_draft, identity_public, make_listing_publishable, ndjson_from_stdout, radroots,
- remove_orderable_listing, replace_latest_listing_event_id, seed_orderable_listing,
+ remove_orderable_listing, replace_latest_listing_event_id, seed_orderable_listing, toml_string,
write_public_identity_profile,
};
@@ -82,6 +82,64 @@ fn removed_global_flags_are_rejected_publicly() {
}
#[test]
+fn config_get_exposes_resolved_publish_state() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
+
+ let value = sandbox.json_success(&["--format", "json", "config", "get"]);
+
+ assert_eq!(value["operation_id"], "config.get");
+ assert_eq!(value["result"]["publish"]["mode"], "radrootsd");
+ assert_eq!(
+ value["result"]["publish"]["source"],
+ "user config · local first"
+ );
+ assert_eq!(value["result"]["publish"]["transport_family"], "radrootsd");
+ assert_eq!(value["result"]["publish"]["state"], "unavailable");
+ assert_eq!(value["result"]["publish"]["executable"], false);
+ assert_eq!(
+ value["result"]["publish"]["provider"]["provider_runtime_id"],
+ "radrootsd"
+ );
+ assert_eq!(value["result"]["write_plane"]["state"], "unavailable");
+}
+
+#[test]
+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",
+ toml_string(missing_myc.display().to_string().as_str())
+ ));
+
+ let value = sandbox.json_success(&["--format", "json", "health", "status", "get"]);
+
+ assert_eq!(value["operation_id"], "health.status.get");
+ assert_eq!(value["result"]["publish"]["mode"], "radrootsd");
+ assert_eq!(value["result"]["publish"]["executable"], false);
+ assert_eq!(
+ value["result"]["publish"]["provider"]["state"],
+ "unavailable"
+ );
+ assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
+}
+
+#[test]
+fn health_check_exposes_publish_readiness() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n");
+
+ let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]);
+
+ assert_eq!(value["operation_id"], "health.check.run");
+ assert_eq!(value["result"]["checks"]["publish"]["mode"], "radrootsd");
+ assert_eq!(value["result"]["checks"]["publish"]["state"], "unavailable");
+ assert_eq!(value["result"]["checks"]["publish"]["executable"], false);
+ assert_eq!(value["errors"].as_array().expect("errors").len(), 0);
+}
+
+#[test]
fn removed_order_submit_watch_flag_is_rejected_publicly() {
let output = radroots()
.args(["order", "submit", "--watch"])