cli

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

commit 16ed7c5d89e0f955d08437f45a76bb5cbbdb0c29
parent 5a0470046729c7dc62748707f54f89f0c2723878
Author: triesap <tyson@radroots.org>
Date:   Wed, 17 Jun 2026 18:21:09 -0700

cli: harden sdk migration guards

- add an explicit allowlist for deferred direct-relay consumers
- table-drive migrated SDK path guards across listing, sync, order, and store
- reject direct relay and legacy replica queue tokens from migrated segments
- cover guard behavior with focused tests and full CLI Nix lanes

Diffstat:
Msrc/runtime/sdk.rs | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
1 file changed, 216 insertions(+), 103 deletions(-)

diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -197,6 +197,22 @@ mod tests { lifecycle: &'static str, } + struct LegacyDirectRelayConsumer { + path: &'static str, + required_tokens: &'static [&'static str], + owner: &'static str, + reason: &'static str, + lifecycle: &'static str, + } + + struct MigratedCliPathGuard { + label: &'static str, + path: &'static str, + start: &'static str, + end: &'static str, + required_tokens: &'static [&'static str], + } + const DIRECT_RR_RS_DEPENDENCIES: &[DirectRrRsDependency] = &[ DirectRrRsDependency { section: "dependencies", @@ -340,6 +356,145 @@ mod tests { }, ]; + const LEGACY_DIRECT_RELAY_CONSUMERS: &[LegacyDirectRelayConsumer] = &[ + LegacyDirectRelayConsumer { + path: "src/runtime/farm.rs", + required_tokens: &[ + "publish_via_direct_relay(", + "publish_signed_event_with_identity", + ], + owner: "farm.publish", + reason: "non-migrated farm publish direct relay write mode", + lifecycle: "retain until farm publish migrates to SDK-backed write APIs", + }, + LegacyDirectRelayConsumer { + path: "src/runtime/listing.rs", + required_tokens: &[ + "mutate_via_direct_relay(", + "publish_signed_event_with_identity", + ], + owner: "listing.nostr_relay.write", + reason: "non-migrated listing direct relay write mode outside SDK local publish", + lifecycle: "retain until listing relay publish migrates to SDK-backed write APIs", + }, + LegacyDirectRelayConsumer { + path: "src/runtime/local_events.rs", + required_tokens: &["DirectRelayFailure", "DirectRelayPublishError"], + owner: "local-event.delivery-evidence", + reason: "delivery evidence mapping for non-migrated direct relay publish outcomes", + lifecycle: "retain until delivery evidence moves behind SDK or local-events APIs", + }, + LegacyDirectRelayConsumer { + path: "src/runtime/order.rs", + required_tokens: &["fetch_events_from_relays", "publish_parts_with_identity"], + owner: "order.lifecycle.preflight-and-mutations", + reason: "non-migrated order lifecycle preflight reads and mutation writes", + lifecycle: "retain until full order lifecycle behavior migrates to SDK APIs", + }, + LegacyDirectRelayConsumer { + path: "src/runtime/sync.rs", + required_tokens: &["fetch_events_from_relays", "pull_with_fetcher"], + owner: "sync.pull-and-market-refresh", + reason: "non-migrated relay ingest into the legacy derived replica", + lifecycle: "retain until relay ingest and derived projection repair migrate to SDK APIs", + }, + LegacyDirectRelayConsumer { + path: "src/runtime/validation_receipt.rs", + required_tokens: &["fetch_events_from_relays", "DirectRelayFetchReceipt"], + owner: "validation.receipt.relay-reads", + reason: "non-migrated validation receipt relay inspection", + lifecycle: "retain until validation receipt inspection migrates to SDK APIs", + }, + ]; + + const MIGRATED_CLI_PATH_GUARDS: &[MigratedCliPathGuard] = &[ + MigratedCliPathGuard { + label: "listing publish", + path: "src/runtime/listing.rs", + start: "pub fn publish_via_sdk(", + end: "fn sdk_listing_publish_input(", + required_tokens: &[ + "session.sdk().listings().prepare_publish", + "session.sdk().listings().enqueue_publish", + "session.sdk().sync().push_outbox", + ], + }, + MigratedCliPathGuard { + label: "sync status", + path: "src/runtime/sync.rs", + start: "pub fn status(config: &RuntimeConfig) -> Result<SyncStatusView, CliSdkAdapterError>", + end: "pub fn pull(", + required_tokens: &["session.sdk().sync().status"], + }, + MigratedCliPathGuard { + label: "sync push", + path: "src/runtime/sync.rs", + start: "pub fn push(config: &RuntimeConfig) -> Result<SyncActionView, CliSdkAdapterError>", + end: "pub fn watch(", + required_tokens: &["session.sdk().sync().push_outbox", "PushOutboxRequest::new"], + }, + MigratedCliPathGuard { + label: "order status", + path: "src/runtime/order.rs", + start: "pub fn status(\n config: &RuntimeConfig", + end: "fn relay_status(", + required_tokens: &["OrderStatusRequest::parse", "session.sdk().orders().status"], + }, + MigratedCliPathGuard { + label: "store status", + path: "src/runtime/store.rs", + start: "pub fn status(config: &RuntimeConfig) -> Result<LocalStatusView, CliSdkAdapterError>", + end: "fn legacy_replica_status(", + required_tokens: &[ + "session.sdk()", + "storage_status(StorageStatusRequest::new())", + "integrity(IntegrityRequest::new())", + ], + }, + MigratedCliPathGuard { + label: "store backup", + path: "src/runtime/store.rs", + start: "pub fn backup(\n config: &RuntimeConfig", + end: "pub fn backup_preflight(", + required_tokens: &["session.sdk().backup", "BackupRequest"], + }, + MigratedCliPathGuard { + label: "store backup preflight", + path: "src/runtime/store.rs", + start: "pub fn backup_preflight(", + end: "pub fn restore(", + required_tokens: &[ + "storage_status(StorageStatusRequest::new())", + "integrity(IntegrityRequest::new())", + ], + }, + MigratedCliPathGuard { + label: "store restore", + path: "src/runtime/store.rs", + start: "pub fn restore(", + end: "pub fn export(", + required_tokens: &[ + "RestoreRequest::new", + "sdk_runtime()", + "RadrootsSdk::restore", + ], + }, + ]; + + const MIGRATED_PATH_DISALLOWED_TOKENS: &[&str] = &[ + "fetch_events_from_relays", + "publish_parts_with_identity", + "publish_via_direct_relay", + "mutate_via_direct_relay", + "radroots_replica_pending_publish", + "radroots_replica_pending_publish_batch", + "radroots_replica_sync_status", + "ReplicaSql::new", + "SqliteExecutor::open(&config.local.replica_db_path)", + "outbox_idempotency_digest", + "canonical_target_relays", + ]; + #[test] fn maps_runtime_config_to_sdk_builder_inputs() { let root = tempdir().expect("tempdir"); @@ -395,11 +550,7 @@ mod tests { let session = CliSdkSession::connect(&config).expect("sdk session"); let status = session - .block_on( - session - .sdk() - .storage_status(StorageStatusRequest::default()), - ) + .block_on(session.sdk().storage_status(StorageStatusRequest::new())) .expect("storage status"); assert_eq!(session.config().storage_root, config.local.root.join("sdk")); @@ -460,93 +611,39 @@ mod tests { } #[test] - fn migrated_cli_paths_are_guarded_against_direct_relay_and_legacy_canonical_use() { - let listing = crate_source("src/runtime/listing.rs"); - assert_migrated_path( - "listing publish", - source_segment( - &listing, - "pub fn publish_via_sdk(", - "fn sdk_listing_publish_input(", - ), - &[ - "session.sdk().listings().prepare_publish", - "session.sdk().listings().enqueue_publish", - "session.sdk().sync().push_outbox", - ], - ); - - let sync = crate_source("src/runtime/sync.rs"); - assert_migrated_path( - "sync status", - source_segment( - &sync, - "pub fn status(config: &RuntimeConfig) -> Result<SyncStatusView, CliSdkAdapterError>", - "pub fn pull(", - ), - &["session.sdk().sync().status"], - ); - assert_migrated_path( - "sync push", - source_segment( - &sync, - "pub fn push(config: &RuntimeConfig) -> Result<SyncActionView, CliSdkAdapterError>", - "pub fn watch(", - ), - &["session.sdk().sync().push_outbox", "PushOutboxRequest::new"], - ); + fn legacy_direct_relay_consumers_are_explicitly_allowlisted() { + let actual = legacy_direct_relay_consumer_paths(); + let expected = LEGACY_DIRECT_RELAY_CONSUMERS + .iter() + .map(|consumer| consumer.path.to_owned()) + .collect::<BTreeSet<_>>(); - let order = crate_source("src/runtime/order.rs"); - assert_migrated_path( - "order status", - source_segment( - &order, - "pub fn status(\n config: &RuntimeConfig", - "fn relay_status(", - ), - &["OrderStatusRequest::parse", "session.sdk().orders().status"], - ); + assert_eq!(actual, expected); + for consumer in LEGACY_DIRECT_RELAY_CONSUMERS { + let source = crate_source(consumer.path); + for token in consumer.required_tokens { + assert!( + source.contains(token), + "{} does not contain legacy direct-relay token `{token}`", + consumer.path + ); + } + assert!(!consumer.owner.trim().is_empty()); + assert!(!consumer.reason.trim().is_empty()); + assert!(!consumer.lifecycle.trim().is_empty()); + } + } - let store = crate_source("src/runtime/store.rs"); - assert_migrated_path( - "store status", - source_segment( - &store, - "pub fn status(config: &RuntimeConfig) -> Result<LocalStatusView, CliSdkAdapterError>", - "fn legacy_replica_status(", - ), - &[ - "session.sdk()", - "storage_status(StorageStatusRequest::new())", - "integrity(IntegrityRequest::new())", - ], - ); - assert_migrated_path( - "store backup", - source_segment( - &store, - "pub fn backup(\n config: &RuntimeConfig", - "pub fn backup_preflight(", - ), - &["session.sdk().backup", "BackupRequest"], - ); - assert_migrated_path( - "store backup preflight", - source_segment(&store, "pub fn backup_preflight(", "pub fn restore("), - &[ - "storage_status(StorageStatusRequest::new())", - "integrity(IntegrityRequest::new())", - ], - ); - assert_migrated_path( - "store restore", - source_segment(&store, "pub fn restore(", "pub fn export("), - &[ - "RestoreRequest::new", - "sdk_runtime()", - "RadrootsSdk::restore", - ], - ); + #[test] + fn migrated_cli_paths_are_guarded_against_direct_relay_and_legacy_canonical_use() { + for guard in MIGRATED_CLI_PATH_GUARDS { + let source = crate_source(guard.path); + assert_migrated_path( + guard.label, + source_segment(&source, guard.start, guard.end), + guard.required_tokens, + ); + } } fn collect_rs_files(dir: &Path, files: &mut Vec<PathBuf>) { @@ -583,6 +680,34 @@ mod tests { format!("{}:{}", dependency.section, dependency.name) } + fn legacy_direct_relay_consumer_paths() -> BTreeSet<String> { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut files = Vec::new(); + collect_rs_files(manifest_dir.join("src/runtime").as_path(), &mut files); + files + .into_iter() + .filter(|file| { + !matches!( + file.file_name().and_then(|name| name.to_str()), + Some("direct_relay.rs" | "sdk.rs") + ) + }) + .filter_map(|file| { + let source = fs::read_to_string(&file).expect("read runtime source"); + source + .contains("use crate::runtime::direct_relay") + .then(|| relative_source_path(manifest_dir, file.as_path())) + }) + .collect() + } + + fn relative_source_path(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .expect("source path under manifest root") + .to_string_lossy() + .replace('\\', "/") + } + fn dependency_path(value: &toml::Value) -> Option<&str> { value .as_table() @@ -611,19 +736,7 @@ mod tests { ); } - for token in [ - "fetch_events_from_relays", - "publish_parts_with_identity", - "publish_via_direct_relay", - "mutate_via_direct_relay", - "radroots_replica_pending_publish", - "radroots_replica_pending_publish_batch", - "radroots_replica_sync_status", - "ReplicaSql::new", - "SqliteExecutor::open(&config.local.replica_db_path)", - "outbox_idempotency_digest", - "canonical_target_relays", - ] { + for token in MIGRATED_PATH_DISALLOWED_TOKENS { assert!( !source.contains(token), "{label} contains disallowed migrated-path token `{token}`"