lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 16bd4f71f9111ac5f7cebc251e35d0529a06c958
parent b014dbfc4a96149d5e027deefe3655f780e401d0
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Jul 2026 05:04:27 +0000

runtime: gate reticulum preview feature matrix

Diffstat:
Mcrates/sdk/src/sync_runtime.rs | 9++++++++-
Mcrates/sdk/tests/source_boundary.rs | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/sdk_xtask_import/src/check.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 145 insertions(+), 1 deletion(-)

diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -5,10 +5,12 @@ use crate::adapters::radrootsd::{ }; #[cfg(feature = "runtime")] use crate::{ - NostrRelayUrlPolicy, ProxyAuth, ProxyProfile, RadrootsSdkError, SyncClient, + NostrRelayUrlPolicy, RadrootsSdkError, SyncClient, runtime::{RadrootsClient, sdk_now_ms}, transport::TransportProfile, }; +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +use crate::{ProxyAuth, ProxyProfile}; #[cfg(feature = "runtime")] use radroots_event_store::{RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX, RadrootsEventStoreStatusSummary}; #[cfg(feature = "runtime")] @@ -629,6 +631,11 @@ impl<'sdk> SyncClient<'sdk> { RadrootsdProxyPublishAdapter::new(radrootsd_proxy_config_from_profile(profile)); self.push_outbox_with_proxy_adapter(&adapter, request).await } + #[cfg(not(feature = "radrootsd-proxy"))] + TransportProfile::Proxy { .. } => Err(RadrootsSdkError::ProductSyncUnsupported { + operation: "sync.push_outbox", + required_feature: "radrootsd-proxy", + }), TransportProfile::LocalOnly => { if self.push_outbox_has_no_ready_signed_work(&request).await? { return Ok(PushOutboxReceipt::default()); diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -1638,6 +1638,67 @@ fn sdk_transport_sources_keep_reticulum_preview_push_boundary() { } } +#[test] +fn sdk_feature_matrix_keeps_reticulum_preview_runtime_owned_without_alias() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let manifest_source = read_source(manifest_dir.join("Cargo.toml").as_path()); + let features_source = source_between(manifest_source.as_str(), "[features]", "[dependencies]"); + let runtime_source = source_between(features_source, "runtime = [", "local-signer = ["); + let nostr_runtime_source = source_between( + features_source, + "transport-nostr-runtime = [", + "local-runtime = [", + ); + + for required in [ + "\"dep:radroots_transport_reticulum\"", + "\"dep:radroots_transport\"", + "\"dep:radroots_outbox\"", + ] { + assert!( + runtime_source.contains(required), + "SDK runtime feature must retain Reticulum preview matrix witness `{required}`" + ); + } + for required in [ + "\"runtime\"", + "\"dep:radroots_nostr\"", + "\"radroots_nostr/client\"", + "\"radroots_transport_nostr/client\"", + ] { + assert!( + nostr_runtime_source.contains(required), + "SDK Nostr runtime feature must retain real delivery matrix witness `{required}`" + ); + } + + for forbidden in [ + "transport-reticulum-preview", + "radroots_transport_reticulum/client", + "reticulum-runtime", + "dep:rns", + "dep:rnsd", + "dep:reticulum", + ] { + assert!( + !manifest_source.contains(forbidden), + "SDK feature matrix must not introduce Reticulum preview alias or real runtime dependency `{forbidden}`" + ); + } + + let sync_runtime_source = read_source(manifest_dir.join("src/sync_runtime.rs").as_path()); + for required in [ + "#[cfg(not(feature = \"radrootsd-proxy\"))]", + "TransportProfile::Proxy { .. } => Err(RadrootsSdkError::ProductSyncUnsupported", + "required_feature: \"radrootsd-proxy\"", + ] { + assert!( + sync_runtime_source.contains(required), + "SDK runtime-only push_outbox must retain proxy feature gate witness `{required}`" + ); + } +} + fn product_runtime_file_stays_on_boundary(relative_path: &str) { let source = read_source( Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/tools/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/src/check.rs @@ -56,6 +56,7 @@ pub fn check() -> Result<(), String> { validate_package_matrix()?; let root = workspace_root()?; validate_sdk_contracts(&root)?; + check_sdk_feature_matrix(&root)?; check_forbidden_packages(&root)?; check_binding_crate_sources(&root)?; check_package_source_metadata(&root)?; @@ -65,6 +66,81 @@ pub fn check() -> Result<(), String> { Ok(()) } +fn check_sdk_feature_matrix(root: &Path) -> Result<(), String> { + let path = root.join("crates/sdk/Cargo.toml"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let features = manifest + .get("features") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} must define [features]", path.display()))?; + let runtime = feature_entries(features, "runtime")?; + for entry in [ + "dep:radroots_transport_reticulum", + "dep:radroots_transport", + "dep:radroots_outbox", + ] { + require_feature_entry(&runtime, "runtime", entry)?; + } + let nostr_runtime = feature_entries(features, "transport-nostr-runtime")?; + for entry in [ + "runtime", + "dep:radroots_nostr", + "radroots_nostr/client", + "radroots_transport_nostr/client", + ] { + require_feature_entry(&nostr_runtime, "transport-nostr-runtime", entry)?; + } + if features.contains_key("transport-reticulum-preview") { + return Err( + "crates/sdk/Cargo.toml must not introduce transport-reticulum-preview as a runtime-owned Reticulum preview feature alias" + .to_owned(), + ); + } + let dependencies = manifest + .get("dependencies") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} must define [dependencies]", path.display()))?; + for dependency in ["rns", "rnsd", "reticulum", "python"] { + if dependencies.contains_key(dependency) { + return Err(format!( + "crates/sdk/Cargo.toml must not add real Reticulum runtime dependency `{dependency}`" + )); + } + } + Ok(()) +} + +fn feature_entries<'features>( + features: &'features toml::map::Map<String, toml::Value>, + feature: &str, +) -> Result<Vec<&'features str>, String> { + features + .get(feature) + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("crates/sdk/Cargo.toml must define feature `{feature}`"))? + .iter() + .map(|entry| { + entry + .as_str() + .ok_or_else(|| format!("feature `{feature}` must contain only string entries")) + }) + .collect() +} + +fn require_feature_entry(entries: &[&str], feature: &str, entry: &str) -> Result<(), String> { + if entries.contains(&entry) { + Ok(()) + } else { + Err(format!( + "crates/sdk/Cargo.toml feature `{feature}` must include `{entry}`" + )) + } +} + fn check_package_source_metadata(root: &Path) -> Result<(), String> { for spec in package_specs() { let package_dir = root.join(spec.package_dir);