lib

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

commit d6bcc3f917ad5aae056687f1932604f19db76a22
parent 89da5d2da4e6b1bd7b37b69ae98a7c2dc990fc83
Author: triesap <tyson@radroots.org>
Date:   Sat, 11 Jul 2026 08:21:46 +0000

mesh-agent: add preview client boundary

- add radroots_mesh_agent_client as the typed mesh-agent client crate
- define schema-aligned status and publish DTOs with serde field names
- provide a mock preview-unavailable client that never reports delivery success
- guard schema hash parity, DTO naming, and preview publish behavior with focused tests

Diffstat:
MCargo.lock | 11+++++++++++
MCargo.toml | 2++
Acrates/mesh_agent_client/Cargo.toml | 28++++++++++++++++++++++++++++
Acrates/mesh_agent_client/README | 17+++++++++++++++++
Acrates/mesh_agent_client/src/lib.rs | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/mesh_agent_client/tests/client.rs | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 505 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4396,6 +4396,17 @@ dependencies = [ ] [[package]] +name = "radroots_mesh_agent_client" +version = "0.1.0-alpha.2" +dependencies = [ + "radroots_mesh", + "radroots_mesh_agent_proto", + "radroots_transport", + "serde", + "serde_json", +] + +[[package]] name = "radroots_mesh_agent_proto" version = "0.1.0-alpha.2" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "crates/transport", "crates/types", "crates/mesh", + "crates/mesh_agent_client", "crates/mesh_agent_proto", "crates/protected_store", "tools/xtask", @@ -106,6 +107,7 @@ radroots_trade = { path = "crates/trade", version = "0.1.0-alpha.2", default-fea radroots_transport = { path = "crates/transport", version = "0.1.0-alpha.2", default-features = false } radroots_types = { path = "crates/types", version = "0.1.0-alpha.2", default-features = false } radroots_mesh = { path = "crates/mesh", version = "0.1.0-alpha.2", default-features = false } +radroots_mesh_agent_client = { path = "crates/mesh_agent_client", version = "0.1.0-alpha.2", default-features = false } radroots_mesh_agent_proto = { path = "crates/mesh_agent_proto", version = "0.1.0-alpha.2", default-features = false } radroots_protected_store = { path = "crates/protected_store", version = "0.1.0-alpha.2", default-features = false } radroots_secret_vault = { path = "crates/secret_vault", version = "0.1.0-alpha.2", default-features = false } diff --git a/crates/mesh_agent_client/Cargo.toml b/crates/mesh_agent_client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "radroots_mesh_agent_client" +publish = false +version = "0.1.0-alpha.2" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "Mesh agent client boundary for Radroots" +repository.workspace = true +homepage.workspace = true +readme = "README" + +[features] +default = ["serde"] +serde = ["dep:serde", "radroots_mesh/serde"] + +[dependencies] +radroots_mesh = { workspace = true, default-features = false } +radroots_mesh_agent_proto = { workspace = true, default-features = false } +radroots_transport = { workspace = true, default-features = false } +serde = { workspace = true, optional = true, features = ["derive", "std"] } + +[dev-dependencies] +serde_json = { workspace = true, features = ["std"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/mesh_agent_client/README b/crates/mesh_agent_client/README @@ -0,0 +1,17 @@ +# radroots_mesh_agent_client + +This is the README for `radroots_mesh_agent_client`, which provides the mesh +agent client boundary for `radroots`. + +## Copyright + +Except as otherwise noted, all files in the `radroots_mesh_agent_client` +distribution are + + Copyright (c) 2025 Tyson Lupul + +## License + +This repository is licensed under either MIT or Apache-2.0, at your option. For +WARRANTIES, see LICENSE included in the `radroots_mesh_agent_client` +distribution. diff --git a/crates/mesh_agent_client/src/lib.rs b/crates/mesh_agent_client/src/lib.rs @@ -0,0 +1,245 @@ +#![forbid(unsafe_code)] + +use radroots_mesh::{ + RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RADROOTS_MESH_PREVIEW_POLICY_ID, + RadrootsMeshPayloadPolicy, +}; +use radroots_mesh_agent_proto::{ + RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, schema_sha256_hex, +}; +use radroots_transport::{ + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportKind, RadrootsTransportMeshScopeId, +}; + +pub const RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID: &str = RADROOTS_MESH_AGENT_SCHEMA_ID; +pub const RADROOTS_MESH_AGENT_CLIENT_SCHEMA_NAMESPACE: &str = RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MeshAgentStatusRequest { + pub include_transports: bool, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MeshAgentStatusResponse { + pub transports: Vec<MeshAgentTransportStatus>, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MeshAgentTransportStatus { + pub transport: MeshAgentTransportKind, + pub profile_id: String, + pub endpoint_uri: String, + pub configured: bool, + pub implementation: MeshAgentImplementation, + pub usable_for_delivery: bool, + pub message: String, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MeshAgentImplementation { + Real, + Mock, + PreviewUnavailable, +} + +impl MeshAgentImplementation { + pub fn schema_name(self) -> &'static str { + match self { + Self::Real => "real", + Self::Mock => "mock", + Self::PreviewUnavailable => "previewUnavailable", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MeshAgentTransportKind { + Reticulum, +} + +impl MeshAgentTransportKind { + pub fn schema_name(self) -> &'static str { + match self { + Self::Reticulum => "reticulum", + } + } + + pub fn transport_kind(self) -> RadrootsTransportKind { + match self { + Self::Reticulum => RadrootsTransportKind::Reticulum, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MeshAgentPublishRequest { + pub publish_request_id: String, + pub payload_cbor: Vec<u8>, + pub event_id: String, + pub target_fingerprint: String, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MeshAgentPublishResponse { + pub publish_request_id: String, + pub status: MeshAgentResponseStatus, + pub transport_receipts: Vec<MeshAgentTransportReceipt>, + pub event_id: String, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MeshAgentResponseStatus { + Accepted, + Deferred, + Rejected, +} + +impl MeshAgentResponseStatus { + pub fn schema_name(self) -> &'static str { + match self { + Self::Accepted => "accepted", + Self::Deferred => "deferred", + Self::Rejected => "rejected", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MeshAgentTransportReceipt { + pub transport_kind: MeshAgentTransportKind, + pub endpoint_uri: String, + pub outcome: MeshAgentTransportOutcome, + pub message: String, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MeshAgentTransportOutcome { + Accepted, + Delivered, + Forwarded, + StoredByGateway, + DeferredUntilImplemented, + Rejected, + RouteUnavailable, + Timeout, + TransportUnavailable, +} + +impl MeshAgentTransportOutcome { + pub fn schema_name(self) -> &'static str { + match self { + Self::Accepted => "accepted", + Self::Delivered => "delivered", + Self::Forwarded => "forwarded", + Self::StoredByGateway => "storedByGateway", + Self::DeferredUntilImplemented => "deferredUntilImplemented", + Self::Rejected => "rejected", + Self::RouteUnavailable => "routeUnavailable", + Self::Timeout => "timeout", + Self::TransportUnavailable => "transportUnavailable", + } + } + + pub fn is_success(self) -> bool { + matches!( + self, + Self::Accepted | Self::Delivered | Self::Forwarded | Self::StoredByGateway + ) + } +} + +pub trait RadrootsMeshAgentClient { + fn status(&self, request: MeshAgentStatusRequest) -> MeshAgentStatusResponse; + fn publish(&self, request: MeshAgentPublishRequest) -> MeshAgentPublishResponse; + fn schema_sha256_hex(&self) -> String; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsMockMeshAgentClient { + profile_id: String, + endpoint_uri: String, + scope: RadrootsTransportMeshScopeId, + policy: RadrootsMeshPayloadPolicy, +} + +impl RadrootsMockMeshAgentClient { + pub fn preview_unavailable() -> Self { + Self { + profile_id: RADROOTS_MESH_PREVIEW_POLICY_ID.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + scope: RadrootsTransportMeshScopeId::local_preview(), + policy: RadrootsMeshPayloadPolicy::preview_unavailable(), + } + } + + pub fn scope(&self) -> &RadrootsTransportMeshScopeId { + &self.scope + } + + pub fn policy(&self) -> &RadrootsMeshPayloadPolicy { + &self.policy + } +} + +impl Default for RadrootsMockMeshAgentClient { + fn default() -> Self { + Self::preview_unavailable() + } +} + +impl RadrootsMeshAgentClient for RadrootsMockMeshAgentClient { + fn status(&self, request: MeshAgentStatusRequest) -> MeshAgentStatusResponse { + let transports = if request.include_transports { + vec![MeshAgentTransportStatus { + transport: MeshAgentTransportKind::Reticulum, + profile_id: self.profile_id.clone(), + endpoint_uri: self.endpoint_uri.clone(), + configured: true, + implementation: MeshAgentImplementation::PreviewUnavailable, + usable_for_delivery: false, + message: RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE.to_owned(), + }] + } else { + Vec::new() + }; + MeshAgentStatusResponse { transports } + } + + fn publish(&self, request: MeshAgentPublishRequest) -> MeshAgentPublishResponse { + MeshAgentPublishResponse { + publish_request_id: request.publish_request_id, + status: MeshAgentResponseStatus::Rejected, + transport_receipts: vec![MeshAgentTransportReceipt { + transport_kind: MeshAgentTransportKind::Reticulum, + endpoint_uri: self.endpoint_uri.clone(), + outcome: MeshAgentTransportOutcome::TransportUnavailable, + message: RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE.to_owned(), + }], + event_id: request.event_id, + } + } + + fn schema_sha256_hex(&self) -> String { + schema_sha256_hex() + } +} diff --git a/crates/mesh_agent_client/tests/client.rs b/crates/mesh_agent_client/tests/client.rs @@ -0,0 +1,202 @@ +use std::fs; +use std::path::Path; + +use radroots_mesh::{RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RADROOTS_MESH_PREVIEW_POLICY_ID}; +use radroots_mesh_agent_client::{ + MeshAgentImplementation, MeshAgentPublishRequest, MeshAgentResponseStatus, + MeshAgentStatusRequest, MeshAgentTransportKind, MeshAgentTransportOutcome, + RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID, RADROOTS_MESH_AGENT_CLIENT_SCHEMA_NAMESPACE, + RadrootsMeshAgentClient, RadrootsMockMeshAgentClient, +}; +use radroots_mesh_agent_proto::{ + RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, schema_sha256_hex, +}; +use radroots_transport::{ + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, + RadrootsTransportKind, +}; +use serde_json::json; + +fn publish_request() -> MeshAgentPublishRequest { + MeshAgentPublishRequest { + publish_request_id: "publish-preview-1".to_owned(), + payload_cbor: vec![1, 2, 3], + event_id: "event-preview-1".to_owned(), + target_fingerprint: "target-preview-1".to_owned(), + } +} + +#[test] +fn mock_client_status_reports_reticulum_preview_unavailable() { + let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let response = client.status(MeshAgentStatusRequest { + include_transports: true, + }); + + assert_eq!(client.scope().as_str(), RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); + assert_eq!(client.policy().policy_id(), RADROOTS_MESH_PREVIEW_POLICY_ID); + assert_eq!(response.transports.len(), 1); + let status = &response.transports[0]; + assert_eq!(status.transport, MeshAgentTransportKind::Reticulum); + assert_eq!( + status.transport.transport_kind(), + RadrootsTransportKind::Reticulum + ); + assert_eq!(status.profile_id, RADROOTS_MESH_PREVIEW_POLICY_ID); + assert_eq!(status.endpoint_uri, RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); + assert!(status.configured); + assert_eq!( + status.implementation, + MeshAgentImplementation::PreviewUnavailable + ); + assert!(!status.usable_for_delivery); + assert_eq!(status.message, RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE); +} + +#[test] +fn mock_client_status_can_omit_transport_rows_without_enabling_delivery() { + let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let response = client.status(MeshAgentStatusRequest { + include_transports: false, + }); + + assert!(response.transports.is_empty()); + assert!(!client.policy().usable_for_delivery()); +} + +#[test] +fn mock_client_publish_never_reports_success_outcomes() { + let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let response = client.publish(publish_request()); + + assert_eq!(response.publish_request_id, "publish-preview-1"); + assert_eq!(response.status, MeshAgentResponseStatus::Rejected); + assert_eq!(response.event_id, "event-preview-1"); + assert_eq!(response.transport_receipts.len(), 1); + let receipt = &response.transport_receipts[0]; + assert_eq!(receipt.transport_kind, MeshAgentTransportKind::Reticulum); + assert_eq!( + receipt.endpoint_uri, + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI + ); + assert_eq!( + receipt.outcome, + MeshAgentTransportOutcome::TransportUnavailable + ); + assert!(!receipt.outcome.is_success()); + assert_eq!(receipt.message, RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE); + for outcome in [ + MeshAgentTransportOutcome::Accepted, + MeshAgentTransportOutcome::Delivered, + MeshAgentTransportOutcome::Forwarded, + MeshAgentTransportOutcome::StoredByGateway, + ] { + assert!(outcome.is_success()); + assert_ne!(receipt.outcome, outcome); + } +} + +#[test] +fn dto_schema_names_match_capnp_surface() { + assert_eq!(MeshAgentTransportKind::Reticulum.schema_name(), "reticulum"); + assert_eq!( + MeshAgentImplementation::PreviewUnavailable.schema_name(), + "previewUnavailable" + ); + assert_eq!(MeshAgentResponseStatus::Accepted.schema_name(), "accepted"); + assert_eq!(MeshAgentResponseStatus::Deferred.schema_name(), "deferred"); + assert_eq!(MeshAgentResponseStatus::Rejected.schema_name(), "rejected"); + assert_eq!( + MeshAgentTransportOutcome::TransportUnavailable.schema_name(), + "transportUnavailable" + ); + assert_eq!( + MeshAgentTransportOutcome::StoredByGateway.schema_name(), + "storedByGateway" + ); +} + +#[test] +fn serde_output_uses_schema_field_names() { + let request = publish_request(); + let request_json = serde_json::to_value(&request).expect("serialize request"); + + assert_eq!( + request_json, + json!({ + "publishRequestId": "publish-preview-1", + "payloadCbor": [1, 2, 3], + "eventId": "event-preview-1", + "targetFingerprint": "target-preview-1" + }) + ); + + let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let response_json = serde_json::to_value(client.publish(request)).expect("serialize response"); + + assert_eq!(response_json["publishRequestId"], "publish-preview-1"); + assert_eq!(response_json["status"], "rejected"); + assert_eq!( + response_json["transportReceipts"][0]["transportKind"], + "reticulum" + ); + assert_eq!( + response_json["transportReceipts"][0]["outcome"], + "transportUnavailable" + ); +} + +#[test] +fn client_schema_hash_matches_proto_authority() { + let client = RadrootsMockMeshAgentClient::preview_unavailable(); + + assert_eq!( + RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID, + RADROOTS_MESH_AGENT_SCHEMA_ID + ); + assert_eq!( + RADROOTS_MESH_AGENT_CLIENT_SCHEMA_NAMESPACE, + RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE + ); + assert_eq!(client.schema_sha256_hex(), schema_sha256_hex()); + assert_eq!( + client.schema_sha256_hex(), + "712aaa11dfec25abf44edb3b0be447f0596442271d46a8b1d9fedb7c3df00bb2" + ); +} + +#[test] +fn client_source_uses_schema_field_names_without_legacy_status_terms() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let source = fs::read_to_string(manifest_dir.join("src/lib.rs")).expect("read source"); + + for required in [ + "MeshAgentStatusRequest", + "include_transports", + "MeshAgentStatusResponse", + "transports", + "MeshAgentTransportStatus", + "profile_id", + "endpoint_uri", + "usable_for_delivery", + "MeshAgentPublishRequest", + "publish_request_id", + "payload_cbor", + "event_id", + "target_fingerprint", + "MeshAgentPublishResponse", + "transport_receipts", + ] { + assert!(source.contains(required), "{required}"); + } + for forbidden in [ + "readiness", + "implementation_state", + "publish_usable", + "fetch_usable", + "redacted_message", + "preview_noop", + ] { + assert!(!source.contains(forbidden), "{forbidden}"); + } +}