commit 9608edf9735169a70a68fbad0a6f48b46a2579b1
parent ccc60e469fee37100764e32eb98265857fc8f88d
Author: triesap <tyson@radroots.org>
Date: Thu, 2 Apr 2026 00:23:32 +0000
custody: bound external command execution time
- add explicit custody timeout config and env parsing for external command identities
- kill hung helper processes and surface a dedicated timeout error instead of stalling runtime operations
- thread the timeout through runtime cli discovery persistence and operability identity loading paths
- cover config validation and external command timeout mapping in myc lib tests
Diffstat:
10 files changed, 270 insertions(+), 38 deletions(-)
diff --git a/.env.example b/.env.example
@@ -2,6 +2,7 @@ MYC_SERVICE_INSTANCE_NAME=myc
MYC_LOGGING_FILTER=info,myc=info
MYC_LOGGING_OUTPUT_DIR=/var/log/radroots/services/myc
MYC_LOGGING_STDOUT=true
+MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS=10
MYC_PATHS_STATE_DIR=/var/lib/myc
MYC_PATHS_SIGNER_IDENTITY_BACKEND=filesystem
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
@@ -3,6 +3,7 @@ use std::future::Future;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
+use std::time::Duration;
use super::backend::MycSignerBackend;
use crate::audit::{
@@ -120,6 +121,7 @@ impl MycRuntime {
&config.persistence,
config.audit.clone(),
MycPolicyContext::from_config(&config.policy)?,
+ Duration::from_secs(config.custody.external_command_timeout_secs),
config.paths.signer_identity_source(),
config.paths.user_identity_source(),
)?;
@@ -1103,13 +1105,14 @@ impl MycSignerContext {
persistence: &MycPersistenceConfig,
audit_config: MycAuditConfig,
policy: MycPolicyContext,
+ external_command_timeout: Duration,
signer_identity_source: MycIdentitySourceSpec,
user_identity_source: MycIdentitySourceSpec,
) -> Result<Self, MycError> {
let signer_identity_provider =
- MycIdentityProvider::from_source("signer", signer_identity_source)?;
+ MycIdentityProvider::from_source("signer", signer_identity_source, external_command_timeout)?;
let user_identity_provider =
- MycIdentityProvider::from_source("user", user_identity_source)?;
+ MycIdentityProvider::from_source("user", user_identity_source, external_command_timeout)?;
let signer_identity = signer_identity_provider.load_active_identity()?;
let user_identity = user_identity_provider.load_active_identity()?;
let signer_store = Self::build_signer_store(persistence, &paths.signer_state_path)?;
diff --git a/src/cli.rs b/src/cli.rs
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
+use std::time::Duration;
use clap::{Args, Parser, Subcommand, ValueEnum};
use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions;
@@ -623,10 +624,12 @@ fn custody_provider_for_role(
MycCustodyRole::Signer => crate::custody::MycIdentityProvider::from_source(
"signer",
config.paths.signer_identity_source(),
+ Duration::from_secs(config.custody.external_command_timeout_secs),
),
MycCustodyRole::User => crate::custody::MycIdentityProvider::from_source(
"user",
config.paths.user_identity_source(),
+ Duration::from_secs(config.custody.external_command_timeout_secs),
),
MycCustodyRole::DiscoveryApp => {
let Some(source) = config.discovery.app_identity_source() else {
@@ -634,7 +637,11 @@ fn custody_provider_for_role(
"discovery app identity is not separately configured; it currently reuses the signer identity".to_owned(),
));
};
- crate::custody::MycIdentityProvider::from_source("discovery app", source)
+ crate::custody::MycIdentityProvider::from_source(
+ "discovery app",
+ source,
+ Duration::from_secs(config.custody.external_command_timeout_secs),
+ )
}
}
}
diff --git a/src/config.rs b/src/config.rs
@@ -20,6 +20,7 @@ pub const DEFAULT_ENV_PATH: &str = ".env";
pub struct MycConfig {
pub service: MycServiceConfig,
pub logging: MycLoggingConfig,
+ pub custody: MycCustodyConfig,
pub paths: MycPathsConfig,
pub persistence: MycPersistenceConfig,
pub audit: MycAuditConfig,
@@ -45,6 +46,12 @@ pub struct MycLoggingConfig {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
+pub struct MycCustodyConfig {
+ pub external_command_timeout_secs: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(default, deny_unknown_fields)]
pub struct MycPathsConfig {
pub state_dir: PathBuf,
pub signer_identity_backend: MycIdentityBackend,
@@ -197,6 +204,7 @@ impl Default for MycConfig {
Self {
service: MycServiceConfig::default(),
logging: MycLoggingConfig::default(),
+ custody: MycCustodyConfig::default(),
paths: MycPathsConfig::default(),
persistence: MycPersistenceConfig::default(),
audit: MycAuditConfig::default(),
@@ -226,6 +234,14 @@ impl Default for MycLoggingConfig {
}
}
+impl Default for MycCustodyConfig {
+ fn default() -> Self {
+ Self {
+ external_command_timeout_secs: 10,
+ }
+ }
+}
+
impl Default for MycPathsConfig {
fn default() -> Self {
Self {
@@ -501,6 +517,11 @@ impl MycConfig {
);
push_env_line(
&mut lines,
+ "MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS",
+ self.custody.external_command_timeout_secs.to_string(),
+ );
+ push_env_line(
+ &mut lines,
"MYC_PATHS_STATE_DIR",
self.paths.state_dir.display().to_string(),
);
@@ -828,6 +849,12 @@ impl MycConfig {
));
}
+ if self.custody.external_command_timeout_secs == 0 {
+ return Err(MycError::InvalidConfig(
+ "custody.external_command_timeout_secs must be greater than zero".to_owned(),
+ ));
+ }
+
validate_identity_source_config(
"paths.signer_identity",
&self.paths.signer_identity_source(),
@@ -1100,6 +1127,10 @@ fn apply_env_entry(
"MYC_LOGGING_STDOUT" => {
config.logging.stdout = parse_bool_env(key, value, path, line_number)?;
}
+ "MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS" => {
+ config.custody.external_command_timeout_secs =
+ parse_u64_env(key, value, path, line_number)?;
+ }
"MYC_PATHS_STATE_DIR" => config.paths.state_dir = PathBuf::from(value),
"MYC_PATHS_SIGNER_IDENTITY_BACKEND" => {
config.paths.signer_identity_backend =
@@ -2194,6 +2225,17 @@ MYC_UNKNOWN=nope
}
#[test]
+ fn validate_rejects_zero_external_command_timeout() {
+ let mut config = MycConfig::default();
+ config.custody.external_command_timeout_secs = 0;
+
+ let err = config.validate().expect_err("invalid custody timeout");
+ assert!(err
+ .to_string()
+ .contains("custody.external_command_timeout_secs"));
+ }
+
+ #[test]
fn validate_rejects_non_loopback_observability_bind_addr() {
let mut config = MycConfig::default();
config.observability.enabled = true;
@@ -2450,6 +2492,7 @@ MYC_DISCOVERY_APP_IDENTITY_KEYRING_SERVICE_NAME=org.radroots.myc.test.discovery
fn parse_and_validate_external_command_identity_backends() {
let config = MycConfig::from_env_str(
r#"
+MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS=21
MYC_PATHS_SIGNER_IDENTITY_BACKEND=external_command
MYC_PATHS_SIGNER_IDENTITY_PATH=/usr/local/libexec/myc-signer-helper
MYC_PATHS_USER_IDENTITY_BACKEND=external_command
@@ -2479,6 +2522,7 @@ MYC_DISCOVERY_APP_IDENTITY_PATH=/usr/local/libexec/myc-discovery-helper
config.discovery.app_identity_backend,
Some(MycIdentityBackend::ExternalCommand)
);
+ assert_eq!(config.custody.external_command_timeout_secs, 21);
assert_eq!(
config
.discovery
@@ -2505,6 +2549,7 @@ MYC_DISCOVERY_APP_IDENTITY_PATH=/usr/local/libexec/myc-discovery-helper
config.logging.output_dir,
Some(PathBuf::from("/var/log/radroots/services/myc"))
);
+ assert_eq!(config.custody.external_command_timeout_secs, 10);
assert_eq!(
config.transport.delivery_policy,
MycTransportDeliveryPolicy::Any
@@ -2540,6 +2585,7 @@ MYC_SERVICE_INSTANCE_NAME=myc-dev
MYC_LOGGING_FILTER=debug,myc=trace
MYC_LOGGING_OUTPUT_DIR=/tmp/myc logs
MYC_LOGGING_STDOUT=false
+MYC_CUSTODY_EXTERNAL_COMMAND_TIMEOUT_SECS=17
MYC_PATHS_STATE_DIR=/tmp/myc state
MYC_PATHS_SIGNER_IDENTITY_BACKEND=os_keyring
MYC_PATHS_SIGNER_IDENTITY_PATH=/tmp/ignored-signer.json
diff --git a/src/custody.rs b/src/custody.rs
@@ -2,6 +2,7 @@ use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Arc;
+use std::time::{Duration, Instant};
use nostr::nips::nip44::Version;
use nostr::nips::{nip04, nip44};
@@ -108,6 +109,7 @@ enum MycIdentityProviderBackend {
},
ExternalCommand {
command_path: PathBuf,
+ timeout: Duration,
executor: Arc<dyn MycExternalCommandExecutor>,
},
}
@@ -155,12 +157,19 @@ struct MycExternalCommandOutput {
stderr: Vec<u8>,
}
+#[derive(Debug)]
+enum MycExternalCommandExecuteError {
+ Io(std::io::Error),
+ TimedOut,
+}
+
trait MycExternalCommandExecutor: Send + Sync {
fn execute(
&self,
command_path: &PathBuf,
request_json: &[u8],
- ) -> Result<MycExternalCommandOutput, std::io::Error>;
+ timeout: Duration,
+ ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError>;
}
#[derive(Debug, Default)]
@@ -171,17 +180,35 @@ impl MycExternalCommandExecutor for MycProcessCommandExecutor {
&self,
command_path: &PathBuf,
request_json: &[u8],
- ) -> Result<MycExternalCommandOutput, std::io::Error> {
+ timeout: Duration,
+ ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError> {
let mut child = Command::new(command_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
- .spawn()?;
+ .spawn()
+ .map_err(MycExternalCommandExecuteError::Io)?;
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
- stdin.write_all(request_json)?;
+ stdin
+ .write_all(request_json)
+ .map_err(MycExternalCommandExecuteError::Io)?;
+ }
+ let deadline = Instant::now() + timeout;
+ loop {
+ match child.try_wait().map_err(MycExternalCommandExecuteError::Io)? {
+ Some(_) => break,
+ None if Instant::now() >= deadline => {
+ let _ = child.kill();
+ let _ = child.wait();
+ return Err(MycExternalCommandExecuteError::TimedOut);
+ }
+ None => std::thread::sleep(Duration::from_millis(10)),
+ }
}
- let output = child.wait_with_output()?;
+ let output = child
+ .wait_with_output()
+ .map_err(MycExternalCommandExecuteError::Io)?;
Ok(MycExternalCommandOutput {
success: output.status.success(),
status: output.status.code(),
@@ -316,6 +343,7 @@ impl MycIdentityOperations for MycLoadedIdentityOperations {
struct MycExternalCommandIdentityOperations {
role: String,
command_path: PathBuf,
+ timeout: Duration,
public_identity: RadrootsIdentityPublic,
public_key: RadrootsNostrPublicKey,
executor: Arc<dyn MycExternalCommandExecutor>,
@@ -325,6 +353,7 @@ impl MycExternalCommandIdentityOperations {
fn new(
role: String,
command_path: PathBuf,
+ timeout: Duration,
public_identity: RadrootsIdentityPublic,
public_key: RadrootsNostrPublicKey,
executor: Arc<dyn MycExternalCommandExecutor>,
@@ -332,6 +361,7 @@ impl MycExternalCommandIdentityOperations {
Self {
role,
command_path,
+ timeout,
public_identity,
public_key,
executor,
@@ -345,11 +375,20 @@ impl MycExternalCommandIdentityOperations {
let request_json = serde_json::to_vec(request)?;
let output = self
.executor
- .execute(&self.command_path, &request_json)
- .map_err(|source| MycError::CustodyExternalCommandIo {
- role: self.role.clone(),
- path: self.command_path.clone(),
- source,
+ .execute(&self.command_path, &request_json, self.timeout)
+ .map_err(|error| match error {
+ MycExternalCommandExecuteError::Io(source) => MycError::CustodyExternalCommandIo {
+ role: self.role.clone(),
+ path: self.command_path.clone(),
+ source,
+ },
+ MycExternalCommandExecuteError::TimedOut => {
+ MycError::CustodyExternalCommandTimedOut {
+ role: self.role.clone(),
+ path: self.command_path.clone(),
+ timeout_secs: self.timeout.as_secs(),
+ }
+ }
})?;
if !output.success {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
@@ -513,6 +552,7 @@ impl MycIdentityProvider {
pub fn from_source(
role: impl Into<String>,
source: MycIdentitySourceSpec,
+ external_command_timeout: Duration,
) -> Result<Self, MycError> {
let role = role.into();
let backend = match source.backend {
@@ -565,6 +605,7 @@ impl MycIdentityProvider {
})?;
MycIdentityProviderBackend::ExternalCommand {
command_path,
+ timeout: external_command_timeout,
executor: Arc::new(MycProcessCommandExecutor),
}
}
@@ -673,16 +714,21 @@ impl MycIdentityProvider {
match &self.backend {
MycIdentityProviderBackend::ExternalCommand {
command_path,
+ timeout,
executor,
} => {
- let (public_identity, public_key) =
- self.load_external_command_identity(command_path, executor.as_ref())?;
+ let (public_identity, public_key) = self.load_external_command_identity(
+ command_path,
+ *timeout,
+ executor.as_ref(),
+ )?;
Ok(MycActiveIdentity::from_operations(
public_identity.clone(),
public_key,
Arc::new(MycExternalCommandIdentityOperations::new(
self.role.clone(),
command_path.clone(),
+ *timeout,
public_identity,
public_key,
executor.clone(),
@@ -820,9 +866,10 @@ impl MycIdentityProvider {
match &self.backend {
MycIdentityProviderBackend::ExternalCommand {
command_path,
+ timeout,
executor,
} => self
- .load_external_command_identity(command_path, executor.as_ref())
+ .load_external_command_identity(command_path, *timeout, executor.as_ref())
.map(|(identity, _)| identity),
_ => self.load_identity().map(|identity| identity.to_public()),
}
@@ -831,6 +878,7 @@ impl MycIdentityProvider {
fn load_external_command_identity(
&self,
command_path: &PathBuf,
+ timeout: Duration,
executor: &dyn MycExternalCommandExecutor,
) -> Result<(RadrootsIdentityPublic, RadrootsNostrPublicKey), MycError> {
let request_json = serde_json::to_vec(&MycExternalCommandRequest {
@@ -841,11 +889,20 @@ impl MycIdentityProvider {
content: None,
})?;
let output = executor
- .execute(command_path, &request_json)
- .map_err(|source| MycError::CustodyExternalCommandIo {
- role: self.role.clone(),
- path: command_path.clone(),
- source,
+ .execute(command_path, &request_json, timeout)
+ .map_err(|error| match error {
+ MycExternalCommandExecuteError::Io(source) => MycError::CustodyExternalCommandIo {
+ role: self.role.clone(),
+ path: command_path.clone(),
+ source,
+ },
+ MycExternalCommandExecuteError::TimedOut => {
+ MycError::CustodyExternalCommandTimedOut {
+ role: self.role.clone(),
+ path: command_path.clone(),
+ timeout_secs: timeout.as_secs(),
+ }
+ }
})?;
if !output.success {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
@@ -1361,7 +1418,8 @@ mod tests {
&self,
_command_path: &PathBuf,
request_json: &[u8],
- ) -> Result<MycExternalCommandOutput, std::io::Error> {
+ _timeout: Duration,
+ ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError> {
let request: MycExternalCommandRequest =
serde_json::from_slice(request_json).expect("request");
self.requests
@@ -1519,6 +1577,7 @@ mod tests {
},
backend: MycIdentityProviderBackend::ExternalCommand {
command_path,
+ timeout: Duration::from_secs(10),
executor: executor.clone(),
},
},
@@ -1536,7 +1595,12 @@ mod tests {
);
let provider =
- MycIdentityProvider::from_source("signer", fixture_source(&path)).expect("provider");
+ MycIdentityProvider::from_source(
+ "signer",
+ fixture_source(&path),
+ Duration::from_secs(10),
+ )
+ .expect("provider");
let identity = provider.load_identity().expect("identity");
assert_eq!(
@@ -1773,4 +1837,84 @@ mod tests {
assert!(!active.nostr_client().has_signer().await);
assert!(!active.nostr_client_owned().has_signer().await);
}
+
+ #[derive(Debug, Default)]
+ struct TimeoutExternalCommandExecutor;
+
+ impl MycExternalCommandExecutor for TimeoutExternalCommandExecutor {
+ fn execute(
+ &self,
+ _command_path: &PathBuf,
+ _request_json: &[u8],
+ _timeout: Duration,
+ ) -> Result<MycExternalCommandOutput, MycExternalCommandExecuteError> {
+ Err(MycExternalCommandExecuteError::TimedOut)
+ }
+ }
+
+ #[test]
+ fn external_command_provider_maps_describe_timeout() {
+ let provider = MycIdentityProvider {
+ role: "signer".to_owned(),
+ source: MycIdentitySourceSpec {
+ backend: MycIdentityBackend::ExternalCommand,
+ path: Some(PathBuf::from("/tmp/signer-helper")),
+ keyring_account_id: None,
+ keyring_service_name: None,
+ profile_path: None,
+ },
+ backend: MycIdentityProviderBackend::ExternalCommand {
+ command_path: PathBuf::from("/tmp/signer-helper"),
+ timeout: Duration::from_secs(7),
+ executor: Arc::new(TimeoutExternalCommandExecutor),
+ },
+ };
+
+ let err = provider.load_active_identity().err().expect("timeout");
+ assert!(matches!(
+ err,
+ MycError::CustodyExternalCommandTimedOut {
+ ref role,
+ ref path,
+ timeout_secs: 7,
+ } if role == "signer" && path == &PathBuf::from("/tmp/signer-helper")
+ ));
+ }
+
+ #[test]
+ fn external_command_provider_maps_operation_timeout() {
+ let identity = RadrootsIdentity::from_secret_key_str(
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ )
+ .expect("identity");
+ let public_identity = identity.to_public();
+ let public_key = identity.public_key();
+ let active = MycActiveIdentity::from_operations(
+ public_identity.clone(),
+ public_key,
+ Arc::new(MycExternalCommandIdentityOperations::new(
+ "signer".to_owned(),
+ PathBuf::from("/tmp/signer-helper"),
+ Duration::from_secs(11),
+ public_identity,
+ public_key,
+ Arc::new(TimeoutExternalCommandExecutor),
+ )),
+ );
+
+ let err = active
+ .sign_event_builder(
+ RadrootsNostrEventBuilder::text_note("timeout"),
+ "timeout event",
+ )
+ .expect_err("timeout");
+ assert!(matches!(
+ err,
+ MycError::CustodyExternalCommandTimedOut {
+ ref role,
+ ref path,
+ timeout_secs: 11,
+ } if role == "signer" && path == &PathBuf::from("/tmp/signer-helper")
+ ));
+ }
}
diff --git a/src/discovery.rs b/src/discovery.rs
@@ -288,9 +288,12 @@ impl MycDiscoveryContext {
}
let app_identity = match discovery.app_identity_source() {
- Some(source) => {
- MycIdentityProvider::from_source("discovery app", source)?.load_active_identity()?
- }
+ Some(source) => MycIdentityProvider::from_source(
+ "discovery app",
+ source,
+ Duration::from_secs(runtime.config().custody.external_command_timeout_secs),
+ )?
+ .load_active_identity()?,
None => runtime.signer_identity().clone(),
};
let public_relays = discovery.resolved_public_relays(&runtime.config().transport)?;
diff --git a/src/error.rs b/src/error.rs
@@ -209,6 +209,14 @@ pub enum MycError {
source: std::io::Error,
},
#[error(
+ "external custody command for {role} identity at {path} timed out after {timeout_secs}s"
+ )]
+ CustodyExternalCommandTimedOut {
+ role: String,
+ path: PathBuf,
+ timeout_secs: u64,
+ },
+ #[error(
"external custody command for {role} identity at {path} failed with status {status}: {stderr}"
)]
CustodyExternalCommandFailed {
diff --git a/src/lib.rs b/src/lib.rs
@@ -26,11 +26,11 @@ pub use audit::{
};
pub use audit_sqlite::MycSqliteOperationAuditStore;
pub use config::{
- DEFAULT_ENV_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycDiscoveryConfig,
- MycDiscoveryMetadataConfig, MycIdentityBackend, MycIdentitySourceSpec, MycLoggingConfig,
- MycObservabilityConfig, MycPathsConfig, MycPersistenceConfig, MycPolicyConfig,
- MycRuntimeAuditBackend, MycServiceConfig, MycSignerStateBackend, MycTransportConfig,
- MycTransportDeliveryPolicy,
+ DEFAULT_ENV_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycCustodyConfig,
+ MycDiscoveryConfig, MycDiscoveryMetadataConfig, MycIdentityBackend, MycIdentitySourceSpec,
+ MycLoggingConfig, MycObservabilityConfig, MycPathsConfig, MycPersistenceConfig,
+ MycPolicyConfig, MycRuntimeAuditBackend, MycServiceConfig, MycSignerStateBackend,
+ MycTransportConfig, MycTransportDeliveryPolicy,
};
pub use control::{MycAcceptedConnectionOutput, MycAuthorizedReplayOutput};
pub use custody::{
diff --git a/src/operability/mod.rs b/src/operability/mod.rs
@@ -411,8 +411,12 @@ fn collect_custody_status(runtime: &MycRuntime) -> Result<MycCustodyStatusEvalua
let discovery_app = if runtime.config().discovery.enabled {
match runtime.config().discovery.app_identity_source() {
Some(source) => Some(
- crate::custody::MycIdentityProvider::from_source("discovery app", source)?
- .probe_status(),
+ crate::custody::MycIdentityProvider::from_source(
+ "discovery app",
+ source,
+ Duration::from_secs(runtime.config().custody.external_command_timeout_secs),
+ )?
+ .probe_status(),
),
None => Some(
runtime
diff --git a/src/persistence.rs b/src/persistence.rs
@@ -1,7 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Component, Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
use nostr::PublicKey;
use radroots_nostr_signer::prelude::{
@@ -452,13 +452,25 @@ pub fn verify_restored_state(
require_existing_restore_file(&delivery_outbox_path, "delivery outbox".to_owned())?;
let signer_identity_provider =
- MycIdentityProvider::from_source("signer", config.paths.signer_identity_source())?;
+ MycIdentityProvider::from_source(
+ "signer",
+ config.paths.signer_identity_source(),
+ Duration::from_secs(config.custody.external_command_timeout_secs),
+ )?;
let signer_identity = signer_identity_provider.load_active_identity()?;
let user_identity_provider =
- MycIdentityProvider::from_source("user", config.paths.user_identity_source())?;
+ MycIdentityProvider::from_source(
+ "user",
+ config.paths.user_identity_source(),
+ Duration::from_secs(config.custody.external_command_timeout_secs),
+ )?;
let user_identity = user_identity_provider.load_active_identity()?;
let discovery_app_identity = match config.discovery.app_identity_source() {
- Some(source) => Some(MycIdentityProvider::from_source("discovery app", source)?),
+ Some(source) => Some(MycIdentityProvider::from_source(
+ "discovery app",
+ source,
+ Duration::from_secs(config.custody.external_command_timeout_secs),
+ )?),
None => None,
}
.map(|provider| provider.load_active_identity())
@@ -548,7 +560,11 @@ fn import_signer_state_json_to_sqlite(
let source_store = RadrootsNostrFileSignerStore::new(&source_path);
let source_state = source_store.load()?;
let signer_identity_provider =
- MycIdentityProvider::from_source("signer", config.paths.signer_identity_source())?;
+ MycIdentityProvider::from_source(
+ "signer",
+ config.paths.signer_identity_source(),
+ Duration::from_secs(config.custody.external_command_timeout_secs),
+ )?;
let configured_signer_identity = signer_identity_provider.load_identity()?.to_public();
if let Some(imported_signer_identity) = source_state.signer_identity.as_ref() {
if imported_signer_identity.id != configured_signer_identity.id {