cli

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

commit 8050a9da1fa7534ecf841af4e2ec75c6ddd0e9c3
parent 0aa0c56d069a20d2911365e91ed1e097da7336ea
Author: triesap <tyson@radroots.org>
Date:   Wed, 17 Jun 2026 15:57:53 -0700

sdk: guard CLI architecture boundary

- move protected store usage to dev-only dependency scope

- classify remaining direct rr-rs dependencies with owners

- guard migrated paths from direct relay and legacy canonical queues

- extend SDK source checks for CLI-specific concepts

Diffstat:
MCargo.toml | 2+-
Msrc/runtime/sdk.rs | 353++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 344 insertions(+), 11 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -36,7 +36,6 @@ radroots_log = { path = "../lib/crates/log" } radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", features = ["os-keyring"] } radroots_nostr = { path = "../lib/crates/nostr", features = ["client", "events"] } 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_db_schema = { path = "../lib/crates/replica_db_schema" } radroots_replica_sync = { path = "../lib/crates/replica_sync" } @@ -58,6 +57,7 @@ zeroize = "1.8" [dev-dependencies] assert_cmd = "2.0" flate2 = "1" +radroots_protected_store = { path = "../lib/crates/protected_store", features = ["std"] } tar = "0.4" tempfile = "3.17" tungstenite = "0.26.2" diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -171,6 +171,7 @@ pub fn sdk_relay_url_policy(config: &RuntimeConfig) -> SdkRelayUrlPolicy { #[cfg(test)] mod tests { + use std::collections::BTreeSet; use std::fs; use std::path::{Path, PathBuf}; @@ -188,6 +189,157 @@ mod tests { RelayPublishPolicy, RhiConfig, RpcConfig, SignerBackend, SignerConfig, Verbosity, }; + struct DirectRrRsDependency { + section: &'static str, + name: &'static str, + owner: &'static str, + reason: &'static str, + lifecycle: &'static str, + } + + const DIRECT_RR_RS_DEPENDENCIES: &[DirectRrRsDependency] = &[ + DirectRrRsDependency { + section: "dependencies", + name: "radroots_authority", + owner: "cli-sdk-adapter", + reason: "local account signer materialization for SDK and remaining CLI-authored signing", + lifecycle: "retain until all signed mutation construction moves behind SDK signer requests", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_core", + owner: "cli-drafts-and-rendering", + reason: "CLI draft parsing, numeric validation, and display DTOs", + lifecycle: "retain while CLI owns TOML draft UX and command rendering", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_events", + owner: "cli-drafts-and-non-migrated-workflows", + reason: "event DTOs for local drafts, views, relay reads, and validation receipt surfaces", + lifecycle: "retain until the remaining event-authoring and inspection surfaces migrate", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_events_codec", + owner: "cli-drafts-and-non-migrated-workflows", + reason: "event encoding and decoding for farm, listing draft, order, sync pull, and validation inspection", + lifecycle: "retain until those command families are SDK-backed", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_identity", + owner: "cli-account-and-signer-ux", + reason: "account identity views, local signer materialization, and direct-relay workflows outside the migrated paths", + lifecycle: "retain while CLI owns account selection and local identity custody UX", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_local_events", + owner: "cli-app-interop", + reason: "shared local work and signed-event interop with the desktop app", + lifecycle: "retain until a shared local-events SDK boundary replaces direct CLI access", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_log", + owner: "cli-runtime-shell", + reason: "CLI logging initialization and file layout", + lifecycle: "permanent CLI runtime ownership", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_nostr", + owner: "non-migrated-direct-relay-workflows", + reason: "direct relay fetch/publish and event conversion for active non-migrated commands", + lifecycle: "retain until direct relay command families migrate or are retired", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_nostr_accounts", + owner: "cli-account-store", + reason: "CLI account selection, import, local signer status, and account persistence", + lifecycle: "retain while CLI owns local account UX and storage", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_nostr_signer", + owner: "cli-signer-readiness", + reason: "signer readiness reporting for active mutation command surfaces", + lifecycle: "retain until signer readiness is fully SDK-owned", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_replica_db", + owner: "legacy-replica-and-market-projection", + reason: "legacy derived replica status, export, market reads, sync pull, basket lookup, and order draft preflight", + lifecycle: "transitional until those derived projection surfaces migrate", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_replica_db_schema", + owner: "legacy-replica-and-market-projection", + reason: "typed query filters for legacy market, basket, and order lookup projections", + lifecycle: "transitional until those derived projection surfaces migrate", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_replica_sync", + owner: "legacy-sync-pull-and-derived-replica", + reason: "legacy relay ingest, sync pull, market refresh, and derived replica state reporting", + lifecycle: "transitional until relay ingest and projection repair move behind SDK APIs", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_runtime", + owner: "cli-config", + reason: "strict environment and config value parsing", + lifecycle: "permanent CLI configuration ownership unless a shared runtime config crate replaces it", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_runtime_paths", + owner: "cli-runtime-paths", + reason: "profile-aware CLI config, data, logs, and secrets path resolution", + lifecycle: "permanent CLI runtime ownership", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_secret_vault", + owner: "cli-account-store", + reason: "local account secret backend selection and readiness", + lifecycle: "retain while CLI owns local account custody UX", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_sp1_host_trade", + owner: "validation-receipts", + reason: "validation receipt SP1 proof inspection and verification", + lifecycle: "retain until validation receipt verification moves behind SDK APIs", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_sql_core", + owner: "legacy-replica-and-local-events", + reason: "SQLite executor for legacy derived replica and shared local-events storage", + lifecycle: "transitional until those storage surfaces move behind SDK or shared runtime APIs", + }, + DirectRrRsDependency { + section: "dependencies", + name: "radroots_trade", + owner: "cli-drafts-and-validation", + reason: "listing draft validation, order economics, order reducer helpers, and validation receipt parsing", + lifecycle: "retain until remaining trade validation and draft behavior migrates", + }, + DirectRrRsDependency { + section: "dev-dependencies", + name: "radroots_protected_store", + owner: "account-tests", + reason: "unit coverage for protected file secret vault behavior", + lifecycle: "test-only", + }, + ]; + #[test] fn maps_runtime_config_to_sdk_builder_inputs() { let root = tempdir().expect("tempdir"); @@ -262,22 +414,132 @@ mod tests { .join("../../../../domains/radroots/sdk/crates/sdk/src"); let mut files = Vec::new(); collect_rs_files(sdk_src.as_path(), &mut files); + let forbidden = [ + ("radroots_cli", "CLI crate identity"), + ("domains/radroots/cli", "CLI mount path"), + ("approval_token", "CLI approval-token UX"), + ("OutputEnvelope", "CLI output envelope"), + ("next_actions", "CLI next-action rendering"), + ("exit_code", "CLI exit-code contract"), + ("docs/", "repository docs path"), + ("radroots store", "CLI command string"), + ("radroots sync", "CLI command string"), + ("radroots listing", "CLI command string"), + ("radroots order", "CLI command string"), + ]; for file in files { let source = fs::read_to_string(&file).expect("read sdk source"); - assert!( - !source.contains("radroots_cli"), - "SDK source imports CLI crate identity in {}", - file.display() - ); - assert!( - !source.contains("domains/radroots/cli"), - "SDK source references CLI path in {}", - file.display() - ); + for (needle, description) in forbidden { + assert!( + !source.contains(needle), + "SDK source contains {description} `{needle}` in {}", + file.display() + ); + } + } + } + + #[test] + fn cli_direct_rr_rs_dependencies_are_classified() { + let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); + let manifest = fs::read_to_string(&manifest_path).expect("read manifest"); + let manifest = manifest.parse::<toml::Value>().expect("parse manifest"); + let actual = direct_rr_rs_dependency_keys(&manifest); + let expected = DIRECT_RR_RS_DEPENDENCIES + .iter() + .map(direct_rr_rs_dependency_key) + .collect::<BTreeSet<_>>(); + + assert_eq!(actual, expected); + for dependency in DIRECT_RR_RS_DEPENDENCIES { + assert!(!dependency.owner.trim().is_empty()); + assert!(!dependency.reason.trim().is_empty()); + assert!(!dependency.lifecycle.trim().is_empty()); } } + #[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"], + ); + + 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"], + ); + + 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::default())", + "integrity(IntegrityRequest::default())", + ], + ); + 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 export("), + &[ + "storage_status(StorageStatusRequest::default())", + "integrity(IntegrityRequest::default())", + ], + ); + } + fn collect_rs_files(dir: &Path, files: &mut Vec<PathBuf>) { for entry in fs::read_dir(dir).expect("read dir") { let path = entry.expect("entry").path(); @@ -289,6 +551,77 @@ mod tests { } } + fn direct_rr_rs_dependency_keys(manifest: &toml::Value) -> BTreeSet<String> { + ["dependencies", "dev-dependencies"] + .into_iter() + .flat_map(|section| { + manifest + .get(section) + .and_then(toml::Value::as_table) + .into_iter() + .flat_map(move |dependencies| { + dependencies.iter().filter_map(move |(name, value)| { + dependency_path(value) + .filter(|path| path.contains("domains/radroots/lib/crates")) + .map(|_| format!("{section}:{name}")) + }) + }) + }) + .collect() + } + + fn direct_rr_rs_dependency_key(dependency: &DirectRrRsDependency) -> String { + format!("{}:{}", dependency.section, dependency.name) + } + + fn dependency_path(value: &toml::Value) -> Option<&str> { + value + .as_table() + .and_then(|table| table.get("path")) + .and_then(toml::Value::as_str) + } + + fn crate_source(path: &str) -> String { + fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(path)).expect("read source") + } + + fn source_segment<'a>(source: &'a str, start: &str, end: &str) -> &'a str { + let start_index = source.find(start).expect("source segment start"); + let end_index = source[start_index..] + .find(end) + .map(|index| start_index + index) + .expect("source segment end"); + &source[start_index..end_index] + } + + fn assert_migrated_path(label: &str, source: &str, required_tokens: &[&str]) { + for token in required_tokens { + assert!( + source.contains(token), + "{label} does not contain required SDK token `{token}`" + ); + } + + 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", + ] { + assert!( + !source.contains(token), + "{label} contains disallowed migrated-path token `{token}`" + ); + } + } + fn sample_config(root: &Path, relays: Vec<String>) -> RuntimeConfig { let data = root.join("data"); let logs = root.join("logs");