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:
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");