lib

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

commit 89da5d2da4e6b1bd7b37b69ae98a7c2dc990fc83
parent 09f3eede085fa13c075906dc4d7ba2ab094e300a
Author: triesap <tyson@radroots.org>
Date:   Sat, 11 Jul 2026 08:10:06 +0000

mesh: add preview admission policy

- add typed mesh privacy, compression, deny-reason, input, decision, and payload-policy models
- expose the Reticulum preview policy id and denial message from radroots_mesh
- make preview_unavailable use zero delivery budgets and disabled compression
- cover all-features and no-default-features policy denial behavior in mesh tests

Diffstat:
Mcrates/mesh/src/lib.rs | 5++++-
Mcrates/mesh/src/model.rs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/mesh/tests/mesh.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 228 insertions(+), 1 deletion(-)

diff --git a/crates/mesh/src/lib.rs b/crates/mesh/src/lib.rs @@ -10,6 +10,9 @@ mod model; pub use cbor::{decode_mesh_frame_cbor, encode_mesh_frame_cbor}; pub use error::RadrootsMeshError; pub use model::{ - RADROOTS_MESH_FRAME_VERSION, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, + RADROOTS_MESH_FRAME_VERSION, RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, + RADROOTS_MESH_PREVIEW_POLICY_ID, RadrootsMeshAdmissionDecision, RadrootsMeshAdmissionInput, + RadrootsMeshCompressionPolicy, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, + RadrootsMeshPayloadPolicy, RadrootsMeshPolicyDenyReason, RadrootsMeshPrivacyClass, RadrootsMeshScope, }; diff --git a/crates/mesh/src/model.rs b/crates/mesh/src/model.rs @@ -4,6 +4,9 @@ use alloc::vec::Vec; use radroots_transport::{RadrootsTransportError, RadrootsTransportMeshScopeId}; pub const RADROOTS_MESH_FRAME_VERSION: u16 = 1; +pub const RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE: &str = + "Reticulum mesh preview is explicit and unavailable for real delivery"; +pub const RADROOTS_MESH_PREVIEW_POLICY_ID: &str = "reticulum_preview_delivery"; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -101,6 +104,173 @@ impl RadrootsMeshScope { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsMeshPrivacyClass { + PublicEvent, + PrivateEvent, +} + +impl RadrootsMeshPrivacyClass { + pub fn label(self) -> &'static str { + match self { + Self::PublicEvent => "public_event", + Self::PrivateEvent => "private_event", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsMeshCompressionPolicy { + Disabled, +} + +impl RadrootsMeshCompressionPolicy { + pub fn label(self) -> &'static str { + match self { + Self::Disabled => "disabled", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsMeshPolicyDenyReason { + PreviewUnavailable, + PayloadBudgetExceeded, + CustomScopeUnavailable, +} + +impl RadrootsMeshPolicyDenyReason { + pub fn label(self) -> &'static str { + match self { + Self::PreviewUnavailable => "preview_unavailable", + Self::PayloadBudgetExceeded => "payload_budget_exceeded", + Self::CustomScopeUnavailable => "custom_scope_unavailable", + } + } + + pub fn message(self) -> &'static str { + match self { + Self::PreviewUnavailable => RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, + Self::PayloadBudgetExceeded => "mesh payload exceeds the configured delivery budget", + Self::CustomScopeUnavailable => { + "mesh custom scopes are not enabled for the configured policy" + } + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsMeshAdmissionDecision { + Accepted, + Denied { + reason: RadrootsMeshPolicyDenyReason, + }, +} + +impl RadrootsMeshAdmissionDecision { + pub fn label(&self) -> &'static str { + match self { + Self::Accepted => "accepted", + Self::Denied { .. } => "denied", + } + } + + pub fn deny_reason(&self) -> Option<RadrootsMeshPolicyDenyReason> { + match self { + Self::Accepted => None, + Self::Denied { reason } => Some(*reason), + } + } + + pub fn message(&self) -> &'static str { + match self { + Self::Accepted => "mesh payload delivery is admitted", + Self::Denied { reason } => reason.message(), + } + } + + pub fn usable_for_delivery(&self) -> bool { + matches!(self, Self::Accepted) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsMeshAdmissionInput { + pub scope: RadrootsMeshScope, + pub privacy_class: RadrootsMeshPrivacyClass, + pub payload_bytes: u64, + pub frame_bytes: u64, +} + +impl RadrootsMeshAdmissionInput { + pub fn new( + scope: RadrootsMeshScope, + privacy_class: RadrootsMeshPrivacyClass, + payload_bytes: u64, + frame_bytes: u64, + ) -> Self { + Self { + scope, + privacy_class, + payload_bytes, + frame_bytes, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsMeshPayloadPolicy { + pub max_payload_bytes: u64, + pub max_frame_bytes: u64, + pub compression: RadrootsMeshCompressionPolicy, + pub custom_scopes_enabled: bool, +} + +impl RadrootsMeshPayloadPolicy { + pub fn preview_unavailable() -> Self { + Self { + max_payload_bytes: 0, + max_frame_bytes: 0, + compression: RadrootsMeshCompressionPolicy::Disabled, + custom_scopes_enabled: false, + } + } + + pub fn policy_id(&self) -> &'static str { + RADROOTS_MESH_PREVIEW_POLICY_ID + } + + pub fn usable_for_delivery(&self) -> bool { + self.max_payload_bytes > 0 && self.max_frame_bytes > 0 + } + + pub fn evaluate(&self, input: &RadrootsMeshAdmissionInput) -> RadrootsMeshAdmissionDecision { + if !self.usable_for_delivery() { + return RadrootsMeshAdmissionDecision::Denied { + reason: RadrootsMeshPolicyDenyReason::PreviewUnavailable, + }; + } + if matches!(input.scope, RadrootsMeshScope::Custom(_)) && !self.custom_scopes_enabled { + return RadrootsMeshAdmissionDecision::Denied { + reason: RadrootsMeshPolicyDenyReason::CustomScopeUnavailable, + }; + } + if input.payload_bytes > self.max_payload_bytes || input.frame_bytes > self.max_frame_bytes + { + return RadrootsMeshAdmissionDecision::Denied { + reason: RadrootsMeshPolicyDenyReason::PayloadBudgetExceeded, + }; + } + RadrootsMeshAdmissionDecision::Accepted + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsMeshPayload { EmptyMap, diff --git a/crates/mesh/tests/mesh.rs b/crates/mesh/tests/mesh.rs @@ -1,5 +1,8 @@ use radroots_mesh::{ + RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RADROOTS_MESH_PREVIEW_POLICY_ID, + RadrootsMeshAdmissionDecision, RadrootsMeshAdmissionInput, RadrootsMeshCompressionPolicy, RadrootsMeshError, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, + RadrootsMeshPayloadPolicy, RadrootsMeshPolicyDenyReason, RadrootsMeshPrivacyClass, RadrootsMeshScope, decode_mesh_frame_cbor, encode_mesh_frame_cbor, }; @@ -76,6 +79,57 @@ fn all_frame_types_round_trip_with_stable_codes_and_labels() { } #[test] +fn preview_policy_has_zero_delivery_budgets_and_disabled_compression() { + let policy = RadrootsMeshPayloadPolicy::preview_unavailable(); + + assert_eq!(policy.policy_id(), RADROOTS_MESH_PREVIEW_POLICY_ID); + assert_eq!(policy.max_payload_bytes, 0); + assert_eq!(policy.max_frame_bytes, 0); + assert_eq!(policy.compression, RadrootsMeshCompressionPolicy::Disabled); + assert_eq!(policy.compression.label(), "disabled"); + assert!(!policy.custom_scopes_enabled); + assert!(!policy.usable_for_delivery()); +} + +#[test] +fn preview_policy_denies_real_payload_admission_deterministically() { + let policy = RadrootsMeshPayloadPolicy::preview_unavailable(); + let input = RadrootsMeshAdmissionInput::new( + RadrootsMeshScope::Local, + RadrootsMeshPrivacyClass::PublicEvent, + 256, + 512, + ); + let decision = policy.evaluate(&input); + + assert_eq!( + decision, + RadrootsMeshAdmissionDecision::Denied { + reason: RadrootsMeshPolicyDenyReason::PreviewUnavailable + } + ); + assert_eq!(decision.label(), "denied"); + assert_eq!( + decision.deny_reason(), + Some(RadrootsMeshPolicyDenyReason::PreviewUnavailable) + ); + assert_eq!(decision.message(), RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE); + assert!(!decision.usable_for_delivery()); + assert_eq!( + RadrootsMeshPrivacyClass::PublicEvent.label(), + "public_event" + ); + assert_eq!( + RadrootsMeshPrivacyClass::PrivateEvent.label(), + "private_event" + ); + assert_eq!( + RadrootsMeshPolicyDenyReason::PreviewUnavailable.label(), + "preview_unavailable" + ); +} + +#[test] fn custom_scope_has_explicit_namespace() { let scope = RadrootsMeshScope::custom("farm-north.preview_1").expect("custom scope"); assert_eq!(scope.label(), "farm-north.preview_1");