cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit bebee95eb806dd4b9c9a566e23c2a6668c89faf7
parent ed50478f8f2afefdf6eaeda96591267e6e87f77f
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Apr 2026 09:38:43 +0000

cli: remove deferred myc runtime path

- delete the inactive MYC status command runner
- fail closed for MYC signer authority without probing executables
- quarantine inactive provider status helpers behind tests
- keep local signer guardrail suites green

Diffstat:
Msrc/runtime/farm.rs | 5-----
Msrc/runtime/listing.rs | 5-----
Msrc/runtime/mod.rs | 2--
Dsrc/runtime/myc.rs | 421-------------------------------------------------------------------------------
Msrc/runtime/order.rs | 5-----
Msrc/runtime/provider.rs | 39++++++++++++++++++++++++++++++++++++++-
Msrc/runtime/signer.rs | 454+++++++------------------------------------------------------------------------
7 files changed, 75 insertions(+), 856 deletions(-)

diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -767,11 +767,6 @@ fn binding_error_publish_view( reason, vec!["run radroots signer status get".to_owned()], ), - ActorWriteBindingError::Unavailable(reason) => ( - "unavailable".to_owned(), - reason, - vec!["run radroots signer status get".to_owned()], - ), }; base_publish_view( state.as_str(), diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -1510,11 +1510,6 @@ fn binding_error_view( reason, vec!["run radroots signer status get".to_owned()], ), - ActorWriteBindingError::Unavailable(reason) => ( - "unavailable".to_owned(), - reason, - vec!["run radroots signer status get".to_owned()], - ), }; ListingMutationView { diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs @@ -2,7 +2,6 @@ pub mod accounts; pub mod config; pub mod daemon; pub mod farm; -#[allow(dead_code)] pub mod farm_config; pub mod find; pub mod hyf; @@ -10,7 +9,6 @@ pub mod listing; pub mod local; pub mod logging; pub mod management; -pub mod myc; pub mod network; pub mod order; pub mod paths; diff --git a/src/runtime/myc.rs b/src/runtime/myc.rs @@ -1,421 +0,0 @@ -use std::io::Read; -use std::process::{Child, Command, ExitStatus, Output, Stdio}; -use std::thread; -use std::time::{Duration, Instant}; - -use radroots_nostr_signer::prelude::{ - RadrootsNostrLocalSignerCapability, RadrootsNostrRemoteSessionSignerCapability, -}; -use serde::Deserialize; - -use crate::domain::runtime::{ - IdentityPublicView, LocalSignerStatusView, MycCustodyIdentityView, MycCustodyView, - MycRemoteSessionView, MycStatusView, -}; -use crate::runtime::config::MycConfig; - -const MYC_SIGNER_STATUS_CONTRACT_VERSION: u32 = 1; -const MYC_STATUS_VIEW: &str = "signer"; -const MYC_STATUS_POLL_INTERVAL: Duration = Duration::from_millis(10); - -pub fn resolve_status(config: &MycConfig) -> MycStatusView { - let executable = config.executable.display().to_string(); - if config.executable.as_os_str().is_empty() { - return unavailable_status( - executable, - "unconfigured", - "myc executable path is not configured".to_owned(), - ); - } - - let output = match run_status_command(config) { - Ok(output) => output, - Err(MycCommandError::NotFound) => { - return unavailable_status( - executable, - "unavailable", - format!( - "myc executable was not found at {}", - config.executable.display() - ), - ); - } - Err(MycCommandError::Start(error)) => { - return unavailable_status( - executable, - "unavailable", - format!( - "failed to start myc status command at {}: {error}", - config.executable.display() - ), - ); - } - Err(MycCommandError::Timeout) => { - return unavailable_status( - executable, - "unavailable", - format!( - "myc status command timed out after {}ms", - config.status_timeout_ms - ), - ); - } - Err(MycCommandError::Wait(error)) | Err(MycCommandError::Read(error)) => { - return unavailable_status( - executable, - "unavailable", - format!( - "failed to capture myc status command output at {}: {error}", - config.executable.display() - ), - ); - } - }; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned(); - let reason = match output.status.code() { - Some(code) if stderr.is_empty() => { - format!("myc status command exited with status code {code}") - } - Some(code) => format!("myc status command exited with status code {code}: {stderr}"), - None if stderr.is_empty() => "myc status command terminated by signal".to_owned(), - None => format!("myc status command terminated by signal: {stderr}"), - }; - return unavailable_status(executable, "unavailable", reason); - } - - let stdout = match String::from_utf8(output.stdout) { - Ok(stdout) => stdout, - Err(error) => { - return unavailable_status( - executable, - "unavailable", - format!("myc status output was not valid UTF-8: {error}"), - ); - } - }; - - let payload_value = match serde_json::from_str::<serde_json::Value>(stdout.as_str()) { - Ok(payload) => payload, - Err(error) => { - return unavailable_status( - executable, - "unavailable", - format!("myc signer status output was not valid JSON: {error}"), - ); - } - }; - let payload = match serde_json::from_value::<MycStatusPayload>(payload_value) { - Ok(payload) => payload, - Err(error) => { - return unavailable_status( - executable, - "unavailable", - format!( - "myc signer status output did not match contract version {MYC_SIGNER_STATUS_CONTRACT_VERSION}: {error}" - ), - ); - } - }; - - let MycStatusPayload { - status_contract_version, - status, - ready, - reasons, - signer_backend, - custody, - } = payload; - if status_contract_version != MYC_SIGNER_STATUS_CONTRACT_VERSION { - return unavailable_status( - executable, - "unavailable", - format!( - "myc signer status contract version {status_contract_version} is incompatible with cli expected {MYC_SIGNER_STATUS_CONTRACT_VERSION}" - ), - ); - } - let MycSignerBackendPayload { - local_signer, - remote_session_count, - remote_sessions, - } = signer_backend; - - let remote_sessions = remote_sessions - .iter() - .map(remote_session_status_view) - .collect::<Vec<_>>(); - let local_signer = local_signer.map(local_signer_status_view); - let remote_session_count = remote_session_count.max(remote_sessions.len()); - let custody = custody.into_view(); - let state = if ready { - "ready" - } else { - match status.as_str() { - "degraded" => "degraded", - _ => "unavailable", - } - }; - let reason = primary_reason(ready, status.as_str(), reasons.as_slice()); - - MycStatusView { - executable, - state: state.to_owned(), - source: "myc status command · local first".to_owned(), - service_status: Some(status), - ready, - reason, - reasons, - remote_session_count, - local_signer, - remote_sessions, - custody, - } -} - -fn run_status_command(config: &MycConfig) -> Result<Output, MycCommandError> { - let mut child = Command::new(&config.executable) - .args(["status", "--view", MYC_STATUS_VIEW]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|error| match error.kind() { - std::io::ErrorKind::NotFound => MycCommandError::NotFound, - _ => MycCommandError::Start(error), - })?; - - let started_at = Instant::now(); - let status_timeout = Duration::from_millis(config.status_timeout_ms); - loop { - match child.try_wait() { - Ok(Some(status)) => return collect_output(child, status), - Ok(None) => { - if started_at.elapsed() >= status_timeout { - let _ = child.kill(); - let _ = child.wait(); - return Err(MycCommandError::Timeout); - } - thread::sleep(MYC_STATUS_POLL_INTERVAL); - } - Err(error) => { - let _ = child.kill(); - let _ = child.wait(); - return Err(MycCommandError::Wait(error)); - } - } - } -} - -fn collect_output(mut child: Child, status: ExitStatus) -> Result<Output, MycCommandError> { - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - - if let Some(mut pipe) = child.stdout.take() { - pipe.read_to_end(&mut stdout) - .map_err(MycCommandError::Read)?; - } - if let Some(mut pipe) = child.stderr.take() { - pipe.read_to_end(&mut stderr) - .map_err(MycCommandError::Read)?; - } - - Ok(Output { - status, - stdout, - stderr, - }) -} - -fn local_signer_status_view( - capability: RadrootsNostrLocalSignerCapability, -) -> LocalSignerStatusView { - LocalSignerStatusView { - account_id: capability.account_id.to_string(), - public_identity: IdentityPublicView::from_public_identity(&capability.public_identity), - availability: match capability.availability { - radroots_nostr_signer::prelude::RadrootsNostrLocalSignerAvailability::PublicOnly => { - "public_only".to_owned() - } - radroots_nostr_signer::prelude::RadrootsNostrLocalSignerAvailability::SecretBacked => { - "secret_backed".to_owned() - } - }, - secret_backed: capability.is_secret_backed(), - backend: "myc".to_owned(), - used_fallback: false, - } -} - -fn remote_session_status_view( - capability: &RadrootsNostrRemoteSessionSignerCapability, -) -> MycRemoteSessionView { - MycRemoteSessionView { - connection_id: capability.connection_id.to_string(), - signer_identity: IdentityPublicView::from_public_identity(&capability.signer_identity), - user_identity: IdentityPublicView::from_public_identity(&capability.user_identity), - relay_count: capability.relays.len(), - permissions: capability - .permissions - .as_slice() - .iter() - .map(ToString::to_string) - .collect(), - } -} - -fn primary_reason(ready: bool, service_status: &str, reasons: &[String]) -> Option<String> { - if ready { - return None; - } - - reasons - .first() - .cloned() - .or_else(|| Some(format!("myc reported service status `{service_status}`"))) -} - -fn unavailable_status(executable: String, state: &str, reason: String) -> MycStatusView { - MycStatusView { - executable, - state: state.to_owned(), - source: "myc status command · local first".to_owned(), - service_status: None, - ready: false, - reason: Some(reason), - reasons: Vec::new(), - remote_session_count: 0, - local_signer: None, - remote_sessions: Vec::new(), - custody: None, - } -} - -enum MycCommandError { - NotFound, - Start(std::io::Error), - Wait(std::io::Error), - Read(std::io::Error), - Timeout, -} - -#[derive(Debug, Deserialize)] -struct MycStatusPayload { - status_contract_version: u32, - status: String, - ready: bool, - #[serde(default)] - reasons: Vec<String>, - #[serde(default)] - signer_backend: MycSignerBackendPayload, - #[serde(default)] - custody: MycCustodyPayload, -} - -#[derive(Debug, Default, Deserialize)] -struct MycSignerBackendPayload { - #[serde(default)] - local_signer: Option<RadrootsNostrLocalSignerCapability>, - #[serde(default)] - remote_session_count: usize, - #[serde(default)] - remote_sessions: Vec<RadrootsNostrRemoteSessionSignerCapability>, -} - -#[derive(Debug, Default, Deserialize)] -struct MycCustodyPayload { - #[serde(default)] - signer: MycCustodyIdentityPayload, - #[serde(default)] - user: MycCustodyIdentityPayload, - #[serde(default)] - discovery_app: Option<MycCustodyIdentityPayload>, -} - -impl MycCustodyPayload { - fn into_view(self) -> Option<MycCustodyView> { - if !self.signer.has_data() - && !self.user.has_data() - && self - .discovery_app - .as_ref() - .is_none_or(|identity| !identity.has_data()) - { - return None; - } - - Some(MycCustodyView { - signer: self.signer.into_view(), - user: self.user.into_view(), - discovery_app: self.discovery_app.and_then(|identity| { - if identity.has_data() { - Some(identity.into_view()) - } else { - None - } - }), - }) - } -} - -#[derive(Debug, Default, Deserialize)] -struct MycCustodyIdentityPayload { - #[serde(default)] - resolved: bool, - #[serde(default)] - selected_account_id: Option<String>, - #[serde(default)] - selected_account_state: Option<String>, - #[serde(default)] - identity_id: Option<String>, - #[serde(default)] - public_key_hex: Option<String>, - #[serde(default)] - error: Option<String>, -} - -impl MycCustodyIdentityPayload { - fn has_data(&self) -> bool { - self.resolved - || self.selected_account_id.is_some() - || self.selected_account_state.is_some() - || self.identity_id.is_some() - || self.public_key_hex.is_some() - || self.error.is_some() - } - - fn into_view(self) -> MycCustodyIdentityView { - MycCustodyIdentityView { - resolved: self.resolved, - selected_account_id: self.selected_account_id, - selected_account_state: self.selected_account_state, - identity_id: self.identity_id, - public_key_hex: self.public_key_hex, - error: self.error, - } - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::resolve_status; - use crate::runtime::config::MycConfig; - - #[test] - fn empty_executable_path_reports_unconfigured_status() { - let view = resolve_status(&MycConfig { - executable: PathBuf::new(), - status_timeout_ms: 2_000, - }); - - assert_eq!(view.state, "unconfigured"); - assert_eq!(view.ready, false); - assert!( - view.reason - .as_deref() - .is_some_and(|value| value.contains("not configured")) - ); - } -} diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -1408,11 +1408,6 @@ fn order_binding_error_view( reason, vec!["run radroots signer status get".to_owned()], ), - ActorWriteBindingError::Unavailable(reason) => ( - "unavailable".to_owned(), - reason, - vec!["run radroots signer status get".to_owned()], - ), }; let mut actions = actions; diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs @@ -1,28 +1,43 @@ -use std::path::{Path, PathBuf}; +#[cfg(test)] +use std::path::Path; +use std::path::PathBuf; use radroots_runtime_manager::{ManagedRuntimeInstallState, load_registry, read_secret_file}; +#[cfg(test)] use radroots_runtime_paths::{ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace, }; +#[cfg(test)] use radroots_sdk::RadrootsSdkConfig; use url::Url; +#[cfg(test)] use crate::runtime::config::{ CapabilityBindingInspection, CapabilityBindingInspectionState, CapabilityBindingTargetKind, INFERENCE_HYF_STDIO_CAPABILITY, RuntimeConfig, WORKFLOW_TRADE_CAPABILITY, WRITE_PLANE_TRADE_JSONRPC_CAPABILITY, }; +#[cfg(not(test))] +use crate::runtime::config::{ + CapabilityBindingTargetKind, RuntimeConfig, WRITE_PLANE_TRADE_JSONRPC_CAPABILITY, +}; +#[cfg(test)] use crate::runtime::hyf; +#[cfg(test)] const WORKFLOW_PROVIDER_RUNTIME_ID: &str = "rhi"; +#[cfg(test)] const WORKFLOW_TARGET: &str = "workflow-default"; +#[cfg(test)] const WORKFLOW_IDENTITY_FILE_NAME: &str = "identity.secret.json"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProviderProvenance { ExplicitBinding, ManagedDefault, + #[cfg(test)] DirectConfig, + #[cfg(test)] Disabled, Unavailable, } @@ -32,13 +47,16 @@ impl ProviderProvenance { match self { Self::ExplicitBinding => "explicit_binding", Self::ManagedDefault => "managed_default", + #[cfg(test)] Self::DirectConfig => "direct_config", + #[cfg(test)] Self::Disabled => "disabled", Self::Unavailable => "unavailable", } } } +#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedProviderView { pub capability_id: String, @@ -70,6 +88,7 @@ pub struct ResolvedWritePlaneTarget { pub bridge_bearer_token: String, } +#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkflowProviderView { pub provider_runtime_id: String, @@ -84,6 +103,7 @@ pub struct WorkflowProviderView { pub reason: Option<String>, } +#[cfg(test)] impl WorkflowProviderView { pub fn detail(&self) -> String { match self.state.as_str() { @@ -104,6 +124,7 @@ impl WorkflowProviderView { } } +#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct HyfProviderView { pub provider_runtime_id: String, @@ -128,6 +149,7 @@ enum WritePlaneResolution { Unconfigured(WritePlaneProviderView), } +#[cfg(test)] pub fn resolve_write_plane_provider(config: &RuntimeConfig) -> WritePlaneProviderView { match resolve_write_plane_resolution(config) { WritePlaneResolution::Ready { view, .. } | WritePlaneResolution::Unconfigured(view) => view, @@ -143,6 +165,7 @@ pub fn resolve_actor_write_plane_target( } } +#[cfg(test)] pub fn resolve_workflow_provider(config: &RuntimeConfig) -> WorkflowProviderView { let binding = inspect_binding(config, WORKFLOW_TRADE_CAPABILITY); let (state, provenance, reason) = match binding.state { @@ -182,6 +205,7 @@ pub fn resolve_workflow_provider(config: &RuntimeConfig) -> WorkflowProviderView } } +#[cfg(test)] pub fn resolve_hyf_provider(config: &RuntimeConfig) -> HyfProviderView { let binding = inspect_binding(config, INFERENCE_HYF_STDIO_CAPABILITY); let status = hyf::resolve_runtime_status(config); @@ -219,6 +243,7 @@ pub fn resolve_hyf_provider(config: &RuntimeConfig) -> HyfProviderView { } } +#[cfg(test)] pub fn resolve_capability_providers(config: &RuntimeConfig) -> Vec<ResolvedProviderView> { let write = resolve_write_plane_provider(config); let workflow = resolve_workflow_provider(config); @@ -490,6 +515,7 @@ fn validate_write_plane_url(value: &str) -> Result<String, String> { Ok(trimmed.to_owned()) } +#[cfg(test)] fn resolve_workflow_execution_state( config: &RuntimeConfig, binding: &CapabilityBindingInspection, @@ -587,6 +613,7 @@ fn resolve_workflow_execution_state( ) } +#[cfg(test)] fn canonical_local_relay_url() -> Result<String, String> { let config = RadrootsSdkConfig::local(); let relays = config @@ -598,12 +625,14 @@ fn canonical_local_relay_url() -> Result<String, String> { .ok_or_else(|| "canonical localhost relay config did not define any relay urls".to_owned()) } +#[cfg(test)] fn canonical_local_radrootsd_url() -> Result<String, String> { RadrootsSdkConfig::local() .resolved_radrootsd_endpoint() .map_err(|error| format!("resolve canonical localhost radrootsd endpoint: {error}")) } +#[cfg(test)] fn workflow_identity_path(repo_local_root: &Path) -> Result<PathBuf, String> { let base_paths = RadrootsPathResolver::current() .resolve( @@ -626,6 +655,7 @@ fn workflow_identity_path(repo_local_root: &Path) -> Result<PathBuf, String> { .join(WORKFLOW_IDENTITY_FILE_NAME)) } +#[cfg(test)] fn loopback_endpoint_matches(configured: &str, canonical: &str) -> bool { let Ok(configured_url) = Url::parse(configured) else { return false; @@ -638,6 +668,7 @@ fn loopback_endpoint_matches(configured: &str, canonical: &str) -> bool { && loopback_host_matches(configured_url.host_str(), canonical_url.host_str()) } +#[cfg(test)] fn loopback_host_matches(left: Option<&str>, right: Option<&str>) -> bool { match (left, right) { (Some(left), Some(right)) => { @@ -647,6 +678,7 @@ fn loopback_host_matches(left: Option<&str>, right: Option<&str>) -> bool { } } +#[cfg(test)] fn normalize_loopback_host(host: &str) -> &str { if host.eq_ignore_ascii_case("localhost") { "127.0.0.1" @@ -655,6 +687,7 @@ fn normalize_loopback_host(host: &str) -> &str { } } +#[cfg(test)] fn inspect_binding(config: &RuntimeConfig, capability_id: &str) -> CapabilityBindingInspection { config .inspect_capability_bindings() @@ -663,6 +696,7 @@ fn inspect_binding(config: &RuntimeConfig, capability_id: &str) -> CapabilityBin .expect("provider capability binding inspection must exist") } +#[cfg(test)] fn binding_provenance(binding: &CapabilityBindingInspection) -> ProviderProvenance { match binding.state { CapabilityBindingInspectionState::Configured => match binding.target_kind.as_deref() { @@ -674,6 +708,7 @@ fn binding_provenance(binding: &CapabilityBindingInspection) -> ProviderProvenan } } +#[cfg(test)] fn hyf_target_kind( config: &RuntimeConfig, binding: &CapabilityBindingInspection, @@ -687,6 +722,7 @@ fn hyf_target_kind( None } +#[cfg(test)] fn hyf_target(config: &RuntimeConfig, binding: &CapabilityBindingInspection) -> Option<String> { if binding.state == CapabilityBindingInspectionState::Configured { return binding.target.clone(); @@ -697,6 +733,7 @@ fn hyf_target(config: &RuntimeConfig, binding: &CapabilityBindingInspection) -> None } +#[cfg(test)] fn hyf_executable( config: &RuntimeConfig, binding: &CapabilityBindingInspection, diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -1,12 +1,9 @@ use crate::domain::runtime::{ - IdentityPublicView, LocalSignerStatusView, MycRemoteSessionView, MycStatusView, - SignerBindingStatusView, SignerStatusView, SignerWriteKindReadinessView, + IdentityPublicView, LocalSignerStatusView, SignerBindingStatusView, SignerStatusView, + SignerWriteKindReadinessView, }; use crate::runtime::accounts::{SHARED_ACCOUNT_STORE_SOURCE, empty_account_resolution_view}; -use crate::runtime::config::{ - CapabilityBindingConfig, CapabilityBindingTargetKind, RuntimeConfig, - SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend, -}; +use crate::runtime::config::{RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend}; use radroots_events::kinds::{KIND_FARM, KIND_LISTING, KIND_PROFILE}; use radroots_events::trade::RadrootsTradeMessageType; use radroots_nostr_accounts::prelude::RadrootsNostrAccountStatus; @@ -18,6 +15,7 @@ use serde::{Deserialize, Serialize}; const SIGNER_BINDING_PROVIDER_RUNTIME_ID: &str = "myc"; const SIGNER_BINDING_MODEL: &str = "session_authorized_remote_signer"; +const MYC_DEFERRED_REASON: &str = "signer mode `myc` is deferred; use signer mode `local`"; #[derive(Debug, Clone, Copy)] struct CliWriteKind { @@ -26,16 +24,8 @@ struct CliWriteKind { } #[derive(Debug, Clone)] -struct MycBindingResolution { - view: SignerBindingStatusView, - resolved_account_id: Option<String>, - resolved_account_public_key_hex: Option<String>, -} - -#[derive(Debug, Clone)] pub enum ActorWriteBindingError { Unconfigured(String), - Unavailable(String), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -55,56 +45,16 @@ pub fn resolve_signer_status(config: &RuntimeConfig) -> SignerStatusView { pub fn resolve_actor_write_authority( config: &RuntimeConfig, - actor_role: &str, - actor_pubkey: &str, + _actor_role: &str, + _actor_pubkey: &str, ) -> Result<Option<ActorWriteSignerAuthority>, ActorWriteBindingError> { if !matches!(config.signer.backend, SignerBackend::Myc) { return Ok(None); } - let myc = crate::runtime::myc::resolve_status(&config.myc); - let resolution = resolve_myc_binding(config, &myc); - match resolution.view.state.as_str() { - "ready" => {} - "unavailable" => { - return Err(ActorWriteBindingError::Unavailable( - resolution.view.reason.unwrap_or_else(|| { - "myc signer binding is unavailable for actor-authored writes".to_owned() - }), - )); - } - _ => { - return Err(ActorWriteBindingError::Unconfigured( - resolution.view.reason.unwrap_or_else(|| { - "myc signer binding is not ready for actor-authored writes".to_owned() - }), - )); - } - } - - let Some(resolved_account_public_key_hex) = resolution.resolved_account_public_key_hex else { - return Err(ActorWriteBindingError::Unconfigured( - "myc signer binding reported ready without a resolved user identity".to_owned(), - )); - }; - - if !resolved_account_public_key_hex.eq_ignore_ascii_case(actor_pubkey) { - return Err(ActorWriteBindingError::Unconfigured(format!( - "configured myc signer binding resolves user pubkey `{resolved_account_public_key_hex}` instead of {actor_role} pubkey `{actor_pubkey}`" - ))); - } - - let Some(resolved_account_id) = resolution.resolved_account_id else { - return Err(ActorWriteBindingError::Unconfigured( - "myc signer binding reported ready without a resolved account identity".to_owned(), - )); - }; - - Ok(Some(ActorWriteSignerAuthority { - provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(), - account_identity_id: resolved_account_id, - provider_signer_session_id: resolution.view.resolved_signer_session_id.clone(), - })) + Err(ActorWriteBindingError::Unconfigured( + MYC_DEFERRED_REASON.to_owned(), + )) } fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView { @@ -273,42 +223,17 @@ fn resolve_myc_signer_status(config: &RuntimeConfig) -> SignerStatusView { Ok(resolution) => crate::runtime::accounts::account_resolution_view(&resolution), Err(_) => empty_account_resolution_view(), }; - let myc = crate::runtime::myc::resolve_status(&config.myc); - let resolution = resolve_myc_binding(config, &myc); - let binding = resolution.view; - let state = myc_signer_state(&myc, &binding).to_owned(); - let resolved_session = binding - .resolved_signer_session_id - .as_deref() - .and_then(|session_id| { - myc.remote_sessions - .iter() - .find(|session| session.connection_id == session_id) - }); - let write_kinds = myc_write_kind_readiness( - resolved_session, - binding.state.as_str(), - binding.reason.as_deref(), - ); SignerStatusView { mode: config.signer.backend.as_str().to_owned(), - state, - source: if myc.state == "ready" { - binding.source.clone() - } else { - myc.source.clone() - }, - signer_account_id: resolution.resolved_account_id, + state: "unconfigured".to_owned(), + source: "target cli signer mode contract".to_owned(), + signer_account_id: None, account_resolution, - reason: if myc.state == "ready" { - binding.reason.clone() - } else { - myc.reason.clone().or_else(|| binding.reason.clone()) - }, - binding, - write_kinds, + reason: Some(MYC_DEFERRED_REASON.to_owned()), + binding: deferred_myc_binding_status(), + write_kinds: deferred_write_kind_readiness(), local: None, - myc: Some(myc), + myc: None, } } @@ -331,293 +256,23 @@ fn disabled_binding_status() -> SignerBindingStatusView { } } -fn resolve_myc_binding(config: &RuntimeConfig, myc: &MycStatusView) -> MycBindingResolution { - let Some(binding) = config.capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY) else { - return MycBindingResolution { - view: SignerBindingStatusView { - capability_id: SIGNER_REMOTE_NIP46_CAPABILITY.to_owned(), - provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(), - binding_model: SIGNER_BINDING_MODEL.to_owned(), - state: "unconfigured".to_owned(), - source: "no explicit capability binding".to_owned(), - target_kind: None, - target: None, - managed_account_ref: None, - signer_session_ref: None, - resolved_signer_session_id: None, - matched_session_count: None, - reason: Some( - "configure [[capability_binding]] for `signer.remote_nip46` before using myc signer mode" - .to_owned(), - ), - }, - resolved_account_id: None, - resolved_account_public_key_hex: None, - }; - }; - - if !matches!( - binding.target_kind, - CapabilityBindingTargetKind::ManagedInstance - ) { - return binding_status( - binding, - "unsupported", - None, - None, - None, - format!( - "signer.remote_nip46 only supports target_kind `managed_instance`; got `{}`", - binding.target_kind.as_str() - ), - ); - } - - if binding.target != "default" { - return binding_status( - binding, - "unsupported", - None, - None, - None, - format!( - "managed myc target `{}` is not supported yet; use target `default`", - binding.target - ), - ); - } - - match myc.state.as_str() { - "ready" => {} - "unconfigured" => { - return binding_status( - binding, - "unconfigured", - None, - None, - None, - myc.reason.clone().unwrap_or_else(|| { - "myc is not configured for composed signer bindings".to_owned() - }), - ); - } - _ => { - return binding_status( - binding, - "unavailable", - None, - None, - None, - myc.reason - .clone() - .unwrap_or_else(|| "myc is not ready for remote signer bindings".to_owned()), - ); - } - } - - let signing_sessions = myc - .remote_sessions - .iter() - .filter(|session| session_supports_signing(session)) - .collect::<Vec<_>>(); - - if let Some(session_ref) = binding.signer_session_ref.as_deref() { - let Some(session) = myc - .remote_sessions - .iter() - .find(|session| session.connection_id == session_ref) - else { - return binding_status( - binding, - "unavailable", - None, - Some(0), - None, - format!("configured signer session `{session_ref}` is not currently available"), - ); - }; - - if !session_supports_signing(session) { - return binding_status( - binding, - "unauthorized", - None, - Some(1), - None, - format!( - "configured signer session `{session_ref}` is not approved for any cli write event kind" - ), - ); - } - - if let Some(account_ref) = binding.managed_account_ref.as_deref() { - if session.user_identity.id != account_ref { - return binding_status( - binding, - "unauthorized", - None, - Some(1), - None, - format!( - "configured signer session `{session_ref}` resolves user `{}` instead of managed account `{account_ref}`", - session.user_identity.id - ), - ); - } - } - - return binding_status( - binding, - "ready", - Some(session.connection_id.clone()), - Some(1), - Some(session), - String::new(), - ); - } - - if let Some(account_ref) = binding.managed_account_ref.as_deref() { - let matching_sessions = signing_sessions - .into_iter() - .filter(|session| session.user_identity.id == account_ref) - .collect::<Vec<_>>(); - return resolve_matching_sessions(binding, account_ref, matching_sessions); - } - - if signing_sessions.is_empty() { - return binding_status( - binding, - "unavailable", - None, - Some(0), - None, - "no authorized remote signer session currently approves a cli write event kind" - .to_owned(), - ); - } - - if signing_sessions.len() > 1 { - return binding_status( - binding, - "ambiguous", - None, - Some(signing_sessions.len()), - None, - "multiple authorized remote signer sessions approve cli write event kinds; set managed_account_ref or signer_session_ref".to_owned(), - ); - } - - let session = signing_sessions - .into_iter() - .next() - .expect("single matching signer session"); - binding_status( - binding, - "ready", - Some(session.connection_id.clone()), - Some(1), - Some(session), - String::new(), - ) -} - -fn resolve_matching_sessions( - binding: &CapabilityBindingConfig, - account_ref: &str, - matching_sessions: Vec<&MycRemoteSessionView>, -) -> MycBindingResolution { - if matching_sessions.is_empty() { - return binding_status( - binding, - "unavailable", - None, - Some(0), - None, - format!( - "no authorized remote signer session currently matches managed account `{account_ref}`" - ), - ); - } - - if matching_sessions.len() > 1 { - return binding_status( - binding, - "ambiguous", - None, - Some(matching_sessions.len()), - None, - format!( - "multiple authorized remote signer sessions currently match managed account `{account_ref}`; set signer_session_ref" - ), - ); - } - - let session = matching_sessions - .into_iter() - .next() - .expect("single matching signer session"); - binding_status( - binding, - "ready", - Some(session.connection_id.clone()), - Some(1), - Some(session), - String::new(), - ) -} - -fn binding_status( - binding: &CapabilityBindingConfig, - state: &str, - resolved_signer_session_id: Option<String>, - matched_session_count: Option<usize>, - resolved_session: Option<&MycRemoteSessionView>, - reason: String, -) -> MycBindingResolution { - MycBindingResolution { - view: SignerBindingStatusView { - capability_id: binding.capability_id.clone(), - provider_runtime_id: binding.provider_runtime_id.clone(), - binding_model: binding.binding_model.clone(), - state: state.to_owned(), - source: binding.source.as_str().to_owned(), - target_kind: Some(binding.target_kind.as_str().to_owned()), - target: Some(binding.target.clone()), - managed_account_ref: binding.managed_account_ref.clone(), - signer_session_ref: binding.signer_session_ref.clone(), - resolved_signer_session_id, - matched_session_count, - reason: if reason.is_empty() { - None - } else { - Some(reason) - }, - }, - resolved_account_id: resolved_session.map(|session| session.user_identity.id.clone()), - resolved_account_public_key_hex: resolved_session - .map(|session| session.user_identity.public_key_hex.clone()), - } -} - -fn myc_signer_state(myc: &MycStatusView, binding: &SignerBindingStatusView) -> &'static str { - match myc.state.as_str() { - "degraded" => "degraded", - "unavailable" => "unavailable", - "unconfigured" => "unconfigured", - _ => match binding.state.as_str() { - "ready" => "ready", - "unavailable" => "unavailable", - _ => "unconfigured", - }, +fn deferred_myc_binding_status() -> SignerBindingStatusView { + SignerBindingStatusView { + capability_id: SIGNER_REMOTE_NIP46_CAPABILITY.to_owned(), + provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(), + binding_model: SIGNER_BINDING_MODEL.to_owned(), + state: "deferred".to_owned(), + source: "target cli signer mode contract".to_owned(), + target_kind: None, + target: None, + managed_account_ref: None, + signer_session_ref: None, + resolved_signer_session_id: None, + matched_session_count: None, + reason: Some(MYC_DEFERRED_REASON.to_owned()), } } -fn session_supports_signing(session: &MycRemoteSessionView) -> bool { - cli_write_kinds() - .iter() - .any(|kind| session_allows_event_kind(session, kind.event_kind)) -} - fn cli_write_kinds() -> [CliWriteKind; 4] { [ CliWriteKind { @@ -655,54 +310,19 @@ fn local_write_kind_readiness( .collect() } -fn myc_write_kind_readiness( - session: Option<&MycRemoteSessionView>, - binding_state: &str, - binding_reason: Option<&str>, -) -> Vec<SignerWriteKindReadinessView> { +fn deferred_write_kind_readiness() -> Vec<SignerWriteKindReadinessView> { cli_write_kinds() .iter() - .map(|kind| { - let permission = format!("sign_event:{}", kind.event_kind); - match session { - Some(session) => { - let ready = session_allows_event_kind(session, kind.event_kind); - SignerWriteKindReadinessView { - command: kind.command.to_owned(), - event_kind: kind.event_kind, - permission, - ready, - reason: if ready { - None - } else { - Some(format!( - "resolved signer session `{}` is not approved for sign_event:{}", - session.connection_id, kind.event_kind - )) - }, - } - } - None => SignerWriteKindReadinessView { - command: kind.command.to_owned(), - event_kind: kind.event_kind, - permission, - ready: false, - reason: binding_reason - .map(str::to_owned) - .or_else(|| Some(format!("signer binding is {binding_state}"))), - }, - } + .map(|kind| SignerWriteKindReadinessView { + command: kind.command.to_owned(), + event_kind: kind.event_kind, + permission: "signer_mode_local_required".to_owned(), + ready: false, + reason: Some(MYC_DEFERRED_REASON.to_owned()), }) .collect() } -fn session_allows_event_kind(session: &MycRemoteSessionView, kind: u32) -> bool { - session - .permissions - .iter() - .any(|permission| permission == "sign_event" || permission == &format!("sign_event:{kind}")) -} - fn local_availability(value: RadrootsNostrLocalSignerAvailability) -> &'static str { match value { RadrootsNostrLocalSignerAvailability::PublicOnly => "public_only",