commit 853eb9704cff329effb47549d70654df2e408404
parent 32cc941b99c79002f34e28b8fcc40b8d7cc11f96
Author: triesap <tyson@radroots.org>
Date: Sat, 13 Jun 2026 16:51:54 -0700
contract: harden sdk ownership metadata
- remove ignored consumer SDK ownership tables
- model rr-rs surface provenance explicitly in xtask
- reject stale consumer SDK tables during contract load
- mark replica WASM bindings as external SDK artifacts
Diffstat:
5 files changed, 158 insertions(+), 30 deletions(-)
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -19,6 +19,7 @@ const EVENT_BOUNDARY_MATRIX_RELATIVES: [&str; 1] = [
];
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
pub struct ContractManifest {
pub contract: ManifestContract,
pub surface: Surface,
@@ -34,10 +35,31 @@ pub struct ManifestContract {
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
pub struct Surface {
pub model_crates: Vec<String>,
pub algorithm_crates: Vec<String>,
pub wasm_crates: Vec<String>,
+ pub rust_crate_tiers: Option<RustCrateTiers>,
+ pub internal_replica_crates: Option<InternalReplicaCrates>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct RustCrateTiers {
+ pub advanced_substrate: Vec<String>,
+ pub published_support: Vec<String>,
+ pub deferred_publication: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct InternalReplicaCrates {
+ pub schema: String,
+ pub storage: String,
+ pub external_storage_wasm_binding_crate: String,
+ pub sync: String,
+ pub external_sync_wasm_binding_crate: String,
}
#[derive(Debug, Deserialize)]
@@ -58,6 +80,7 @@ pub struct ManifestLanguagePackages {
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
pub struct OperationsContractManifest {
pub contract: ManifestContract,
pub public: PublicContract,
@@ -2535,6 +2558,70 @@ fn validate_sdk_rollout(mapping: &SdkExportMapping) -> Result<(), String> {
Ok(())
}
+fn validate_crate_identifier(value: &str, field: &str) -> Result<(), String> {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ return Err(format!("{field} is required"));
+ }
+ if trimmed != value
+ || trimmed.contains('/')
+ || trimmed.contains('\\')
+ || trimmed.contains("..")
+ || trimmed == "radroots_sdk"
+ {
+ return Err(format!("{field} must be a crate identifier"));
+ }
+ Ok(())
+}
+
+fn validate_surface_metadata(surface: &Surface) -> Result<(), String> {
+ if let Some(tiers) = &surface.rust_crate_tiers {
+ let mut tier_crates = BTreeSet::new();
+ for (field, crates) in [
+ (
+ "surface.rust_crate_tiers.advanced_substrate",
+ &tiers.advanced_substrate,
+ ),
+ (
+ "surface.rust_crate_tiers.published_support",
+ &tiers.published_support,
+ ),
+ (
+ "surface.rust_crate_tiers.deferred_publication",
+ &tiers.deferred_publication,
+ ),
+ ] {
+ let entries = collect_unique_set(crates, field)?;
+ if entries.is_empty() {
+ return Err(format!("{field} must not be empty"));
+ }
+ for crate_name in entries {
+ if !tier_crates.insert(crate_name.clone()) {
+ return Err(format!(
+ "surface.rust_crate_tiers has duplicate crate {crate_name}"
+ ));
+ }
+ }
+ }
+ }
+
+ if let Some(replica) = &surface.internal_replica_crates {
+ validate_crate_identifier(&replica.schema, "surface.internal_replica_crates.schema")?;
+ validate_crate_identifier(&replica.storage, "surface.internal_replica_crates.storage")?;
+ validate_crate_identifier(&replica.sync, "surface.internal_replica_crates.sync")?;
+ validate_crate_identifier(
+ &replica.external_storage_wasm_binding_crate,
+ "surface.internal_replica_crates.external_storage_wasm_binding_crate",
+ )?;
+ validate_crate_identifier(
+ &replica.external_sync_wasm_binding_crate,
+ "surface.internal_replica_crates.external_sync_wasm_binding_crate",
+ )?;
+ }
+
+ Ok(())
+}
+
fn validate_operations_contract(
bundle: &ContractBundle,
operations_manifest: &OperationsContractManifest,
@@ -3612,6 +3699,7 @@ fn validate_contract_bundle_with_release_policy_override(
if bundle.manifest.surface.algorithm_crates.is_empty() {
return Err("contract surface.algorithm_crates must not be empty".to_string());
}
+ validate_surface_metadata(&bundle.manifest.surface)?;
validate_export_mappings(bundle)?;
if bundle.version.contract.version.trim().is_empty() {
return Err("version.contract.version is required".to_string());
@@ -3980,6 +4068,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> {
if bundle.manifest.surface.algorithm_crates.is_empty() {
return Err("contract surface.algorithm_crates must not be empty".to_string());
}
+ validate_surface_metadata(&bundle.manifest.surface)?;
validate_export_mappings(bundle)?;
if bundle.version.contract.version.trim().is_empty() {
return Err("version.contract.version is required".to_string());
@@ -5556,7 +5645,10 @@ edition = "2024"
let assert_bundle_error = |expected: &str, mutator: fn(&mut ContractBundle)| {
let mut bundle = load_contract_bundle(&root).expect("load bundle");
mutator(&mut bundle);
- let err = validate_contract_bundle(&bundle).expect_err("bundle validation error");
+ let err = match validate_contract_bundle(&bundle) {
+ Ok(()) => panic!("expected bundle validation error: {expected}"),
+ Err(err) => err,
+ };
assert!(err.contains(expected), "expected `{expected}` in `{err}`");
};
@@ -5576,6 +5668,18 @@ edition = "2024"
bundle.manifest.surface.algorithm_crates.clear();
});
assert_bundle_error(
+ "surface.internal_replica_crates.external_storage_wasm_binding_crate must be a crate identifier",
+ |bundle| {
+ bundle.manifest.surface.internal_replica_crates = Some(InternalReplicaCrates {
+ schema: "radroots_replica_db_schema".to_string(),
+ storage: "radroots_replica_db".to_string(),
+ external_storage_wasm_binding_crate: "crates/replica_db_wasm".to_string(),
+ sync: "radroots_replica_sync".to_string(),
+ external_sync_wasm_binding_crate: "radroots_replica_sync_wasm".to_string(),
+ });
+ },
+ );
+ assert_bundle_error(
"at least one language export mapping is required",
|bundle| {
bundle.exports.clear();
@@ -5677,6 +5781,42 @@ edition = "2024"
}
#[test]
+ fn load_contract_bundle_rejects_stale_consumer_sdk_tables() {
+ let stale_manifest_root = create_synthetic_workspace("stale_manifest_consumer_sdk");
+ let manifest_path = stale_manifest_root.join("spec").join("manifest.toml");
+ let mut manifest = fs::read_to_string(&manifest_path).expect("manifest");
+ manifest.push_str(
+ r#"
+[consumer_sdk]
+rust_package = "radroots_sdk"
+"#,
+ );
+ write_file(&manifest_path, &manifest);
+ let manifest_err =
+ load_contract_bundle(&stale_manifest_root).expect_err("stale manifest table");
+ assert!(manifest_err.contains("manifest.toml"));
+ assert!(manifest_err.contains("consumer_sdk"));
+ let _ = fs::remove_dir_all(stale_manifest_root);
+
+ let stale_operations_root = create_synthetic_workspace("stale_operations_consumer_sdk");
+ add_operation_contract_files(&stale_operations_root);
+ let operations_path = stale_operations_root.join("spec").join("operations.toml");
+ let mut operations = fs::read_to_string(&operations_path).expect("operations");
+ operations.push_str(
+ r#"
+[consumer_sdk]
+rust_package = "radroots_sdk"
+"#,
+ );
+ write_file(&operations_path, &operations);
+ let operations_err =
+ load_contract_bundle(&stale_operations_root).expect_err("stale operations table");
+ assert!(operations_err.contains("operations.toml"));
+ assert!(operations_err.contains("consumer_sdk"));
+ let _ = fs::remove_dir_all(stale_operations_root);
+ }
+
+ #[test]
fn validate_contract_bundle_reports_operation_contract_errors() {
let root = create_synthetic_workspace("operation_contract_bundle_errors");
add_operation_contract_files(&root);
diff --git a/spec/README.md b/spec/README.md
@@ -11,15 +11,20 @@ machine-verifiable.
## Contract Surface
-SDK contract metadata is defined in `spec/manifest.toml` and currently includes:
+Core contract metadata is defined in `spec/manifest.toml` and currently includes:
- model crates: `radroots_core`, `radroots_events`, `radroots_trade`, `radroots_identity`
- algorithm crate: `radroots_events_codec`
-The first-class Rust SDK and WebAssembly package surfaces are owned by the SDK
-repository. The crate list above records rr-rs implementation provenance for the
-core contract surface; it is not a promise that every listed crate is a
-first-class end-user SDK package.
+The first-class Rust SDK and WebAssembly package surfaces are owned outside
+rr-rs by the SDK repository. The crate list above records rr-rs implementation
+provenance for the core contract surface; it is not a promise that every listed
+crate is a first-class end-user SDK package.
+
+`spec/manifest.toml` and `spec/operations.toml` do not carry consumer SDK
+ownership tables. Curated language package authority lives under
+`spec/sdk-exports/`; SDK-owned Rust and WebAssembly package assembly lives in
+the SDK repository.
Public SDK exports are intentionally narrower than the full Rust workspace.
@@ -155,8 +160,9 @@ Internal replica crate family:
- `radroots_replica_db`
- `radroots_replica_sync`
-SDK-owned wasm bindings for replica storage and sync are recorded in
-`spec/replica.toml`.
+External SDK-owned wasm binding artifact identifiers for replica storage and
+sync are recorded in `spec/replica.toml`. They are provenance identifiers, not
+local rr-rs crate paths or runnable package commands.
## Governance
diff --git a/spec/manifest.toml b/spec/manifest.toml
@@ -13,13 +13,7 @@ model_crates = [
algorithm_crates = ["radroots_events_codec"]
wasm_crates = []
-[consumer_sdk]
-rust_package = "radroots_sdk"
-public_surface = "operation_first"
-website_ingest_contract = true
-
-[consumer_sdk.rust_story]
-entrypoints = ["radroots_sdk"]
+[surface.rust_crate_tiers]
advanced_substrate = [
"radroots_core",
"radroots_events",
@@ -65,9 +59,9 @@ deferred_publication = [
[surface.internal_replica_crates]
schema = "radroots_replica_db_schema"
storage = "radroots_replica_db"
-storage_wasm_binding_crate = "radroots_replica_db_wasm"
+external_storage_wasm_binding_crate = "radroots_replica_db_wasm"
sync = "radroots_replica_sync"
-sync_wasm_binding_crate = "radroots_replica_sync_wasm"
+external_sync_wasm_binding_crate = "radroots_replica_sync_wasm"
[export.ts]
packages = ["@radroots/sdk"]
diff --git a/spec/operations.toml b/spec/operations.toml
@@ -55,18 +55,6 @@ model_crates = [
algorithm_crates = ["radroots_events_codec"]
wasm_crates = []
-[consumer_sdk]
-rust_package = "radroots_sdk"
-primary_domains = [
- "profile",
- "farm",
- "listing",
- "order",
- "trade_validation",
- "social",
-]
-public_surface = "operation_first"
-
[operations.profile_build_draft]
domain = "profile"
id = "profile.build_draft"
diff --git a/spec/replica.toml b/spec/replica.toml
@@ -8,7 +8,7 @@ schema = "radroots_replica_db_schema"
storage = "radroots_replica_db"
sync = "radroots_replica_sync"
-[sdk_wasm_bindings]
+[external_sdk_wasm_bindings]
storage = "radroots_replica_db_wasm"
sync = "radroots_replica_sync_wasm"