commit cfcc7c99d3cbf6a215d34f7a04ebb4ae1bd3590e
parent 7ed8d3580bd7756de3145c384d5790bf6645cceb
Author: triesap <tyson@radroots.org>
Date: Thu, 9 Jul 2026 05:04:27 +0000
runtime: gate reticulum preview feature matrix
Diffstat:
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/xtask/src/check.rs b/tools/xtask/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);