lib

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

commit e0e93b24f2a2e49a2d5391e6897d2eac21fec6bc
parent 3eedbd320c52a9b035cd9f94cc389497c5b04e7f
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 15:07:58 +0000

protocol: migrate runtime operation contract v1

- Move all 57 V1 operation identities into the final versioned protocol module.
- Preserve request and receipt schema IDs plus serialized policy descriptor values.
- Validate catalog uniqueness, delivery routes, schema dispatch, and parser behavior.
- Pass focused, no-std, WASM, architecture, hygiene, and full contract checks.

Diffstat:
Mcrates/protocol/src/runtime.rs | 2+-
Acrates/protocol/src/runtime/v1.rs | 1180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/protocol/src/schema.rs | 17++++++++++++++---
Mcrates/protocol/tests/package_boundary.rs | 5++---
4 files changed, 1197 insertions(+), 7 deletions(-)

diff --git a/crates/protocol/src/runtime.rs b/crates/protocol/src/runtime.rs @@ -1,4 +1,4 @@ //! Versioned runtime operation contracts. /// Runtime operation contracts for generation 1. -pub mod v1 {} +pub mod v1; diff --git a/crates/protocol/src/runtime/v1.rs b/crates/protocol/src/runtime/v1.rs @@ -0,0 +1,1180 @@ +//! Runtime operation descriptor contract generation 1. +//! +//! This module owns passive, serialized operation identities and policy +//! descriptors. Native SDK operation implementations and host execution state +//! deliberately remain outside the wire-contract package. + +use alloc::{ + collections::BTreeSet, + format, + string::{String, ToString}, + vec::Vec, +}; +use core::fmt; + +use crate::{ + capability::v1::{Maturity, TransportKind}, + schema::{Descriptor as SchemaDescriptor, ModuleVersion, Registry}, +}; + +/// Schema generation shared by every runtime operation request and receipt. +pub const OPERATION_SCHEMA_VERSION: u16 = 1; + +macro_rules! operation_ids { + ($( $variant:ident => $value:literal ),+ $(,)?) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum OperationId { + $( $variant, )+ + } + + impl OperationId { + pub fn as_str(self) -> &'static str { + match self { + $( Self::$variant => $value, )+ + } + } + + pub fn parse(value: &str) -> Result<Self, Error> { + match value { + $( $value => Ok(Self::$variant), )+ + _ => Err(Error::UnknownOperationId { + operation_id: value.to_string(), + }), + } + } + + pub fn request_schema_id(self) -> &'static str { + match self { + $( Self::$variant => concat!("radroots.runtime.", $value, ".request.v1"), )+ + } + } + + pub fn receipt_schema_id(self) -> &'static str { + match self { + $( Self::$variant => concat!("radroots.runtime.", $value, ".receipt.v1"), )+ + } + } + } + + #[cfg(feature = "serde")] + impl serde::Serialize for OperationId { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } + } + + #[cfg(feature = "serde")] + impl<'de> serde::Deserialize<'de> for OperationId { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let value = <String as serde::Deserialize>::deserialize(deserializer)?; + Self::parse(value.as_str()).map_err(serde::de::Error::custom) + } + } + }; +} + +operation_ids! { + ProfileInspect => "profile.inspect", + ProfileReset => "profile.reset", + AccountCreate => "account.create", + AccountImport => "account.import", + AccountSelect => "account.select", + AccountList => "account.list", + AccountRemove => "account.remove", + SignerStatus => "signer.status", + StoreInspect => "store.inspect", + StoreBackup => "store.backup", + StoreRestore => "store.restore", + FarmCreate => "farm.create", + FarmUpdate => "farm.update", + FarmPublish => "farm.publish", + FarmGet => "farm.get", + FarmList => "farm.list", + ListingCreate => "listing.create", + ListingUpdate => "listing.update", + ListingPublish => "listing.publish", + ListingPause => "listing.pause", + ListingWithdraw => "listing.withdraw", + ListingGet => "listing.get", + ListingList => "listing.list", + MarketPull => "market.pull", + MarketSearch => "market.search", + MarketGet => "market.get", + BasketCreate => "basket.create", + BasketGet => "basket.get", + BasketList => "basket.list", + BasketItemAdd => "basket.item.add", + BasketItemUpdate => "basket.item.update", + BasketItemRemove => "basket.item.remove", + BasketQuote => "basket.quote", + TradeProposalSubmit => "trade.proposal.submit", + TradeRevisionPropose => "trade.revision.propose", + TradeCandidateDecide => "trade.candidate.decide", + TradeCancellationSubmit => "trade.cancellation.submit", + TradeOperationResume => "trade.operation.resume", + TradeGet => "trade.get", + TradeList => "trade.list", + TradeEvidenceRefresh => "trade.evidence.refresh", + TradeEvidenceInspect => "trade.evidence.inspect", + TradePrivateArtifactSeal => "trade.private_artifact.seal", + TradePrivateArtifactOpen => "trade.private_artifact.open", + TradePrivateArtifactDelete => "trade.private_artifact.delete", + ValidationStatus => "validation.status", + SyncStatus => "sync.status", + SyncPull => "sync.pull", + SyncPush => "sync.push", + HealthInspect => "health.inspect", + TransportCapabilityList => "transport.capability.list", + TransportConfigInspect => "transport.config.inspect", + TransportConfigUpdate => "transport.config.update", + TransportStatusInspect => "transport.status.inspect", + TransportDeliveryInspect => "transport.delivery.inspect", + TransportDeliveryRetry => "transport.delivery.retry", + DiagnosticsInspect => "diagnostics.inspect", +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Mutability { + Read, + Mutation, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Risk { + Low, + Medium, + High, + Critical, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ApprovalRequirement { + None, + ConditionalOrRequiredByMode, + Required, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SignerRequirement { + None, + Required, + ConditionalRelayAuth, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IdempotencyPolicy { + Forbidden, + RequiredUuidV7, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DryRunSupport { + NotApplicable, + PureLocalPlan, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DeadlinePolicy { + DefaultBounded, + OperationDeclared, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PrivacyEffect { + None, + PublicEvent, + PrivateCoordination, + PrivateStore, + BackupRestore, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ProjectionEffect { + None, + ReadsProjection, + WritesProjection, + MayUpdateProjection, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TransportRoute { + pub local: bool, + pub nostr: bool, + pub reticulum: bool, + pub deliver: bool, + pub fetch: bool, + pub synchronize: bool, + pub diagnostics: bool, +} + +impl TransportRoute { + pub const fn none() -> Self { + Self { + local: false, + nostr: false, + reticulum: false, + deliver: false, + fetch: false, + synchronize: false, + diagnostics: false, + } + } + + pub const fn local() -> Self { + Self { + local: true, + nostr: false, + reticulum: false, + deliver: false, + fetch: false, + synchronize: false, + diagnostics: false, + } + } + + pub const fn delivery() -> Self { + Self { + local: false, + nostr: true, + reticulum: true, + deliver: true, + fetch: false, + synchronize: false, + diagnostics: false, + } + } + + pub const fn fetch() -> Self { + Self { + local: false, + nostr: true, + reticulum: true, + deliver: false, + fetch: true, + synchronize: true, + diagnostics: false, + } + } + + pub const fn diagnostics() -> Self { + Self { + local: true, + nostr: true, + reticulum: true, + deliver: false, + fetch: false, + synchronize: false, + diagnostics: true, + } + } + + pub fn includes_transport(self, kind: TransportKind) -> bool { + match kind { + TransportKind::Local => self.local, + TransportKind::Nostr => self.nostr, + TransportKind::Reticulum => self.reticulum, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct OperationDescriptor { + pub operation_id: OperationId, + pub schema_version: u16, + pub mutability: Mutability, + pub risk: Risk, + pub approval: ApprovalRequirement, + pub signer: SignerRequirement, + pub transport_capability: TransportRoute, + pub idempotency: IdempotencyPolicy, + pub dry_run: DryRunSupport, + pub deadline: DeadlinePolicy, + pub privacy: PrivacyEffect, + pub projection: ProjectionEffect, + pub maturity: Maturity, +} + +impl OperationDescriptor { + pub fn request_schema_id(self) -> &'static str { + self.operation_id.request_schema_id() + } + + pub fn receipt_schema_id(self) -> &'static str { + self.operation_id.receipt_schema_id() + } +} + +struct DescriptorSpec { + operation_id: OperationId, + mutability: Mutability, + risk: Risk, + approval: ApprovalRequirement, + signer: SignerRequirement, + transport_capability: TransportRoute, + idempotency: IdempotencyPolicy, + dry_run: DryRunSupport, + privacy: PrivacyEffect, + projection: ProjectionEffect, +} + +const fn read( + operation_id: OperationId, + risk: Risk, + transport_capability: TransportRoute, + privacy: PrivacyEffect, + projection: ProjectionEffect, +) -> OperationDescriptor { + descriptor(DescriptorSpec { + operation_id, + mutability: Mutability::Read, + risk, + approval: ApprovalRequirement::None, + signer: SignerRequirement::None, + transport_capability, + idempotency: IdempotencyPolicy::Forbidden, + dry_run: DryRunSupport::NotApplicable, + privacy, + projection, + }) +} + +const fn mutation( + operation_id: OperationId, + risk: Risk, + approval: ApprovalRequirement, + signer: SignerRequirement, + transport_capability: TransportRoute, + privacy: PrivacyEffect, + projection: ProjectionEffect, +) -> OperationDescriptor { + descriptor(DescriptorSpec { + operation_id, + mutability: Mutability::Mutation, + risk, + approval, + signer, + transport_capability, + idempotency: IdempotencyPolicy::RequiredUuidV7, + dry_run: DryRunSupport::PureLocalPlan, + privacy, + projection, + }) +} + +const fn descriptor(spec: DescriptorSpec) -> OperationDescriptor { + OperationDescriptor { + operation_id: spec.operation_id, + schema_version: OPERATION_SCHEMA_VERSION, + mutability: spec.mutability, + risk: spec.risk, + approval: spec.approval, + signer: spec.signer, + transport_capability: spec.transport_capability, + idempotency: spec.idempotency, + dry_run: spec.dry_run, + deadline: DeadlinePolicy::DefaultBounded, + privacy: spec.privacy, + projection: spec.projection, + maturity: Maturity::Stable, + } +} + +pub const CATALOG: &[OperationDescriptor] = &[ + read( + OperationId::ProfileInspect, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::ProfileReset, + Risk::Critical, + ApprovalRequirement::Required, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::AccountCreate, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::AccountImport, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::AccountSelect, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::AccountList, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::AccountRemove, + Risk::Critical, + ApprovalRequirement::Required, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::SignerStatus, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::None, + ), + read( + OperationId::StoreInspect, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::StoreBackup, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::BackupRestore, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::StoreRestore, + Risk::Critical, + ApprovalRequirement::Required, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::BackupRestore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::FarmCreate, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::FarmUpdate, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::FarmPublish, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PublicEvent, + ProjectionEffect::MayUpdateProjection, + ), + read( + OperationId::FarmGet, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::FarmList, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::ListingCreate, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::ListingUpdate, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::ListingPublish, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PublicEvent, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::ListingPause, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PublicEvent, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::ListingWithdraw, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PublicEvent, + ProjectionEffect::MayUpdateProjection, + ), + read( + OperationId::ListingGet, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::ListingList, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::MarketPull, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::ConditionalRelayAuth, + TransportRoute::fetch(), + PrivacyEffect::None, + ProjectionEffect::MayUpdateProjection, + ), + read( + OperationId::MarketSearch, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::MarketGet, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::BasketCreate, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::BasketGet, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::BasketList, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::BasketItemAdd, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::BasketItemUpdate, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::BasketItemRemove, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::BasketQuote, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + mutation( + OperationId::TradeProposalSubmit, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::TradeRevisionPropose, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::TradeCandidateDecide, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::TradeCancellationSubmit, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::TradeOperationResume, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::Required, + TransportRoute::delivery(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::MayUpdateProjection, + ), + read( + OperationId::TradeGet, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::TradeList, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::TradeEvidenceRefresh, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::TradeEvidenceInspect, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::TradePrivateArtifactSeal, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::TradePrivateArtifactOpen, + Risk::High, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::TradePrivateArtifactDelete, + Risk::Critical, + ApprovalRequirement::Required, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateCoordination, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::ValidationStatus, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::SyncStatus, + Risk::Low, + TransportRoute::diagnostics(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::SyncPull, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::ConditionalRelayAuth, + TransportRoute::fetch(), + PrivacyEffect::None, + ProjectionEffect::MayUpdateProjection, + ), + mutation( + OperationId::SyncPush, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::ConditionalRelayAuth, + TransportRoute::delivery(), + PrivacyEffect::PublicEvent, + ProjectionEffect::MayUpdateProjection, + ), + read( + OperationId::HealthInspect, + Risk::Low, + TransportRoute::diagnostics(), + PrivacyEffect::None, + ProjectionEffect::None, + ), + read( + OperationId::TransportCapabilityList, + Risk::Low, + TransportRoute::diagnostics(), + PrivacyEffect::None, + ProjectionEffect::None, + ), + read( + OperationId::TransportConfigInspect, + Risk::Low, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::TransportConfigUpdate, + Risk::High, + ApprovalRequirement::ConditionalOrRequiredByMode, + SignerRequirement::None, + TransportRoute::local(), + PrivacyEffect::PrivateStore, + ProjectionEffect::WritesProjection, + ), + read( + OperationId::TransportStatusInspect, + Risk::Low, + TransportRoute::diagnostics(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + read( + OperationId::TransportDeliveryInspect, + Risk::Low, + TransportRoute::diagnostics(), + PrivacyEffect::None, + ProjectionEffect::ReadsProjection, + ), + mutation( + OperationId::TransportDeliveryRetry, + Risk::Medium, + ApprovalRequirement::None, + SignerRequirement::ConditionalRelayAuth, + TransportRoute::delivery(), + PrivacyEffect::PublicEvent, + ProjectionEffect::MayUpdateProjection, + ), + read( + OperationId::DiagnosticsInspect, + Risk::Low, + TransportRoute::diagnostics(), + PrivacyEffect::None, + ProjectionEffect::None, + ), +]; + +/// Returns the descriptor for an exact operation identity. +pub fn operation_descriptor(operation_id: OperationId) -> Result<OperationDescriptor, Error> { + CATALOG + .iter() + .copied() + .find(|descriptor| descriptor.operation_id == operation_id) + .ok_or_else(|| Error::UnknownOperationId { + operation_id: operation_id.as_str().to_string(), + }) +} + +/// Validates uniqueness, schema generation, and descriptor policy invariants. +pub fn validate_catalog(descriptors: &[OperationDescriptor]) -> Result<(), Error> { + let mut operation_ids = BTreeSet::new(); + for descriptor in descriptors { + if !operation_ids.insert(descriptor.operation_id) { + return Err(Error::DuplicateOperationId { + operation_id: descriptor.operation_id, + }); + } + match (descriptor.mutability, descriptor.idempotency) { + (Mutability::Read, IdempotencyPolicy::Forbidden) + | (Mutability::Mutation, IdempotencyPolicy::RequiredUuidV7) => {} + _ => { + return Err(Error::CatalogInvalid { + message: format!( + "operation {} has invalid idempotency policy", + descriptor.operation_id.as_str() + ), + }); + } + } + if descriptor.schema_version != OPERATION_SCHEMA_VERSION { + return Err(Error::UnsupportedOperationSchemaVersion { + operation_id: descriptor.operation_id, + version: descriptor.schema_version, + }); + } + } + + for required in [ + OperationId::TransportCapabilityList, + OperationId::TransportConfigInspect, + OperationId::TransportConfigUpdate, + OperationId::TransportStatusInspect, + OperationId::TransportDeliveryInspect, + OperationId::TransportDeliveryRetry, + OperationId::SyncStatus, + OperationId::SyncPull, + OperationId::SyncPush, + OperationId::DiagnosticsInspect, + ] { + if !operation_ids.contains(&required) { + return Err(Error::MissingRequiredOperation { + operation_id: required, + }); + } + } + + for delivery in [ + OperationId::FarmPublish, + OperationId::ListingPublish, + OperationId::ListingPause, + OperationId::ListingWithdraw, + OperationId::TradeProposalSubmit, + OperationId::TradeRevisionPropose, + OperationId::TradeCandidateDecide, + OperationId::TradeCancellationSubmit, + OperationId::TradeOperationResume, + OperationId::SyncPush, + OperationId::TransportDeliveryRetry, + ] { + let descriptor = descriptors + .iter() + .find(|descriptor| descriptor.operation_id == delivery) + .ok_or(Error::MissingRequiredOperation { + operation_id: delivery, + })?; + if !descriptor.transport_capability.deliver + || !descriptor + .transport_capability + .includes_transport(TransportKind::Nostr) + || !descriptor + .transport_capability + .includes_transport(TransportKind::Reticulum) + { + return Err(Error::CatalogInvalid { + message: format!( + "operation {} must use Nostr and Reticulum delivery capability", + descriptor.operation_id.as_str() + ), + }); + } + } + Ok(()) +} + +/// Builds the registry for every exact request and receipt schema identity. +pub fn schema_registry() -> Result<Registry, crate::schema::Error> { + let mut schemas = Vec::with_capacity(CATALOG.len() * 2); + for descriptor in CATALOG { + schemas.push(SchemaDescriptor::try_new( + descriptor.request_schema_id(), + ModuleVersion::RuntimeV1, + )?); + schemas.push(SchemaDescriptor::try_new( + descriptor.receipt_schema_id(), + ModuleVersion::RuntimeV1, + )?); + } + Registry::try_new(schemas) +} + +/// Runtime operation catalog validation failure. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Error { + /// An operation identity appears more than once. + DuplicateOperationId { + /// Duplicated operation identity. + operation_id: OperationId, + }, + /// A required operation is missing from a candidate catalog. + MissingRequiredOperation { + /// Missing operation identity. + operation_id: OperationId, + }, + /// An operation identity is unknown. + UnknownOperationId { + /// Rejected identity. + operation_id: String, + }, + /// An operation declares a non-V1 schema generation. + UnsupportedOperationSchemaVersion { + /// Operation with the incompatible version. + operation_id: OperationId, + /// Rejected version. + version: u16, + }, + /// A descriptor invariant is violated. + CatalogInvalid { + /// Secret-safe validation diagnostic. + message: String, + }, +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DuplicateOperationId { operation_id } => { + write!( + formatter, + "duplicate operation id {}", + operation_id.as_str() + ) + } + Self::MissingRequiredOperation { operation_id } => { + write!( + formatter, + "missing required operation {}", + operation_id.as_str() + ) + } + Self::UnknownOperationId { operation_id } => { + write!(formatter, "unknown operation id {operation_id}") + } + Self::UnsupportedOperationSchemaVersion { + operation_id, + version, + } => write!( + formatter, + "unsupported operation schema version {version} for {}", + operation_id.as_str() + ), + Self::CatalogInvalid { message } => formatter.write_str(message), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn catalog_is_unique_and_preserves_every_v1_operation() { + validate_catalog(CATALOG).expect("runtime operation catalog"); + assert_eq!(CATALOG.len(), 57); + + let identities = CATALOG + .iter() + .map(|descriptor| descriptor.operation_id.as_str()) + .collect::<BTreeSet<_>>(); + assert_eq!(identities.len(), CATALOG.len()); + for descriptor in CATALOG { + assert_eq!( + OperationId::parse(descriptor.operation_id.as_str()), + Ok(descriptor.operation_id) + ); + } + } + + #[test] + fn parser_is_exact_and_rejects_retired_preview_names() { + assert_eq!( + OperationId::parse("trade.proposal.submit"), + Ok(OperationId::TradeProposalSubmit) + ); + assert_eq!( + OperationId::parse("runtime.unknown") + .expect_err("unknown") + .to_string(), + "unknown operation id runtime.unknown" + ); + for value in [ + ["sync.try_reticulum", "_preview_now"].concat(), + ["transport.reticulum", "_preview.status"].concat(), + ["transport.", "hybrid", ".publish"].concat(), + ["radrootsd.", "proxy", ".publish"].concat(), + ] { + assert!(OperationId::parse(value.as_str()).is_err()); + } + } + + #[test] + fn schema_ids_and_registry_preserve_v1_vectors() { + let profile = operation_descriptor(OperationId::ProfileInspect).expect("profile"); + assert_eq!( + profile.request_schema_id(), + "radroots.runtime.profile.inspect.request.v1" + ); + assert_eq!( + profile.receipt_schema_id(), + "radroots.runtime.profile.inspect.receipt.v1" + ); + + let registry = schema_registry().expect("runtime schema registry"); + assert_eq!(registry.len(), CATALOG.len() * 2); + assert!( + registry + .descriptors() + .iter() + .all(|descriptor| descriptor.module() == ModuleVersion::RuntimeV1) + ); + } + + #[test] + fn validation_rejects_duplicates_and_policy_drift() { + assert_eq!( + validate_catalog(&[CATALOG[0], CATALOG[0]]), + Err(Error::DuplicateOperationId { + operation_id: OperationId::ProfileInspect, + }) + ); + + let mut invalid = CATALOG.to_vec(); + invalid[0].idempotency = IdempotencyPolicy::RequiredUuidV7; + assert_eq!( + validate_catalog(invalid.as_slice()), + Err(Error::CatalogInvalid { + message: "operation profile.inspect has invalid idempotency policy".into(), + }) + ); + } + + #[cfg(feature = "serde")] + #[test] + fn serialized_descriptor_vector_is_exact() { + let descriptor = operation_descriptor(OperationId::ProfileInspect).expect("profile"); + assert_eq!( + serde_json::to_value(descriptor).expect("descriptor JSON"), + serde_json::json!({ + "operation_id": "profile.inspect", + "schema_version": 1, + "mutability": "read", + "risk": "low", + "approval": "none", + "signer": "none", + "transport_capability": { + "local": true, + "nostr": false, + "reticulum": false, + "deliver": false, + "fetch": false, + "synchronize": false, + "diagnostics": false + }, + "idempotency": "forbidden", + "dry_run": "not_applicable", + "deadline": "default_bounded", + "privacy": "private_store", + "projection": "reads_projection", + "maturity": "stable" + }) + ); + assert_eq!( + serde_json::to_string(&OperationId::ProfileInspect).expect("operation JSON"), + "\"profile.inspect\"" + ); + } +} diff --git a/crates/protocol/src/schema.rs b/crates/protocol/src/schema.rs @@ -268,7 +268,16 @@ pub fn protocol_v1_registry() -> Result<Registry, Error> { .iter() .copied() .map(|metadata| (metadata, ModuleVersion::EventV1)); - Registry::try_from_metadata(capability.chain(event)) + let mut descriptors = Registry::try_from_metadata(capability.chain(event))? + .descriptors() + .to_vec(); + descriptors.extend( + crate::runtime::v1::schema_registry()? + .descriptors() + .iter() + .cloned(), + ); + Registry::try_new(descriptors) } /// Schema identity or registry validation failure. @@ -509,9 +518,11 @@ mod tests { #[test] fn protocol_v1_registry_dispatches_all_migrated_schemas() { let registry = protocol_v1_registry().expect("protocol V1 registry"); - assert_eq!(registry.len(), 5); + assert_eq!(registry.len(), 5 + crate::runtime::v1::CATALOG.len() * 2); for descriptor in registry.descriptors() { - let expected = if descriptor.id().as_str().contains("event_descriptor") + let expected = if descriptor.id().as_str().starts_with("radroots.runtime.") { + ModuleVersion::RuntimeV1 + } else if descriptor.id().as_str().contains("event_descriptor") || descriptor.id().as_str().contains("trade_state") { ModuleVersion::EventV1 diff --git a/crates/protocol/tests/package_boundary.rs b/crates/protocol/tests/package_boundary.rs @@ -46,9 +46,8 @@ fn crate_root_exposes_only_the_approved_versioned_skeleton() { for source in [CAPABILITY, EVENT] { assert!(source.lines().any(|line| line.trim() == "pub mod v1;")); } - for source in [ERROR, RUNTIME] { - assert!(source.lines().any(|line| line.trim() == "pub mod v1 {}")); - } + assert!(ERROR.lines().any(|line| line.trim() == "pub mod v1 {}")); + assert!(RUNTIME.lines().any(|line| line.trim() == "pub mod v1;")); assert!(RADROOTSD.contains("pub mod transport_publish {")); assert!(RADROOTSD.lines().any(|line| line.trim() == "pub mod v5 {}")); assert!(