commit 97d31a05a998aa1304908b825a2bd31de03df2e5
parent ca37b85c4531f350f4b3ae1e5067d3a58349028c
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Jul 2026 10:56:15 +0000
mesh_agent_proto: validate schema structure
- replace substring guards with schema-aware declaration validation
- reject commented misplaced duplicate and type-drifted declarations
- keep validation crate-local without runtime or IPC behavior
- prove the mesh agent schema gate through release preflight
Diffstat:
3 files changed, 740 insertions(+), 56 deletions(-)
diff --git a/crates/mesh_agent_proto/src/lib.rs b/crates/mesh_agent_proto/src/lib.rs
@@ -7,12 +7,15 @@ use alloc::string::String;
use core::fmt;
use sha2::{Digest, Sha256};
+mod schema_validation;
+
pub const RADROOTS_MESH_AGENT_SCHEMA_ID: &str = "0xb83e0c4f71838d9a";
pub const RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE: &str = "radroots::mesh_agent::v1";
pub const RADROOTS_MESH_AGENT_SCHEMA: &str = include_str!("../schema/radroots_mesh_agent_v1.capnp");
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RadrootsMeshAgentProtoError {
+ InvalidSchema,
MissingSchemaId,
MissingNamespace,
MissingRequest,
@@ -27,6 +30,7 @@ pub enum RadrootsMeshAgentProtoError {
impl fmt::Display for RadrootsMeshAgentProtoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
+ Self::InvalidSchema => f.write_str("mesh agent schema is invalid"),
Self::MissingSchemaId => f.write_str("mesh agent schema id is missing"),
Self::MissingNamespace => f.write_str("mesh agent schema namespace is missing"),
Self::MissingRequest => f.write_str("mesh agent request schema is missing"),
@@ -65,60 +69,5 @@ pub fn validate_schema() -> Result<(), RadrootsMeshAgentProtoError> {
}
pub fn validate_schema_text(schema: &str) -> Result<(), RadrootsMeshAgentProtoError> {
- if !schema.contains(RADROOTS_MESH_AGENT_SCHEMA_ID) {
- return Err(RadrootsMeshAgentProtoError::MissingSchemaId);
- }
- if !schema.contains(RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE) {
- return Err(RadrootsMeshAgentProtoError::MissingNamespace);
- }
- if !schema.contains("struct MeshAgentRequest") || !schema.contains("frameCbor @2 :Data;") {
- return Err(RadrootsMeshAgentProtoError::MissingRequest);
- }
- if !schema.contains("enum MeshAgentAction")
- || !schema.contains("validateFrame @0;")
- || !schema.contains("stageDelivery @1;")
- || !schema.contains("observeEventHead @2;")
- || !schema.contains("status @3;")
- || !schema.contains("publish @4;")
- {
- return Err(RadrootsMeshAgentProtoError::MissingAction);
- }
- if !schema.contains("struct MeshAgentResponse")
- || !schema.contains("enum MeshAgentResponseStatus")
- {
- return Err(RadrootsMeshAgentProtoError::MissingResponse);
- }
- if !schema.contains("struct MeshAgentReceipt")
- || !schema.contains("acceptedEventHeads @1 :List(Text);")
- {
- return Err(RadrootsMeshAgentProtoError::MissingReceipt);
- }
- if !schema.contains("struct MeshAgentStatusRequest")
- || !schema.contains("struct MeshAgentStatusResponse")
- || !schema.contains("enum MeshAgentReadinessState")
- || !schema.contains("enum MeshAgentImplementationState")
- || !schema.contains("struct MeshAgentTransportStatus")
- || !schema.contains("includeTransports @0 :Bool;")
- || !schema.contains("readiness @0 :MeshAgentReadinessState;")
- || !schema.contains("implementationState @1 :MeshAgentImplementationState;")
- || !schema.contains("publishUsable @5 :Bool;")
- || !schema.contains("fetchUsable @6 :Bool;")
- || !schema.contains("previewUnavailable @3;")
- {
- return Err(RadrootsMeshAgentProtoError::MissingStatusSurface);
- }
- if !schema.contains("struct MeshAgentPublishRequest")
- || !schema.contains("struct MeshAgentPublishResponse")
- || !schema.contains("struct MeshAgentTransportReceipt")
- || !schema.contains("publishRequest @4 :MeshAgentPublishRequest;")
- || !schema.contains("publishResponse @5 :MeshAgentPublishResponse;")
- || !schema.contains("publishRequestId @0 :Text;")
- || !schema.contains("transportReceipts @2 :List(MeshAgentTransportReceipt);")
- {
- return Err(RadrootsMeshAgentProtoError::MissingPublishSurface);
- }
- if !schema.contains("struct MeshAgentError") {
- return Err(RadrootsMeshAgentProtoError::MissingError);
- }
- Ok(())
+ schema_validation::validate_schema_text(schema)
}
diff --git a/crates/mesh_agent_proto/src/schema_validation.rs b/crates/mesh_agent_proto/src/schema_validation.rs
@@ -0,0 +1,662 @@
+use alloc::collections::{BTreeMap, BTreeSet};
+use alloc::string::String;
+use alloc::vec::Vec;
+
+use crate::{
+ RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE,
+ RadrootsMeshAgentProtoError,
+};
+
+struct RequiredField {
+ owner: &'static str,
+ name: &'static str,
+ ordinal: u16,
+ field_type: &'static str,
+}
+
+struct RequiredVariant {
+ owner: &'static str,
+ name: &'static str,
+ ordinal: u16,
+}
+
+const REQUEST_FIELDS: &[RequiredField] = &[
+ field("MeshAgentRequest", "requestId", 0, "Text"),
+ field("MeshAgentRequest", "action", 1, "MeshAgentAction"),
+ field("MeshAgentRequest", "frameCbor", 2, "Data"),
+ field(
+ "MeshAgentRequest",
+ "statusRequest",
+ 3,
+ "MeshAgentStatusRequest",
+ ),
+ field(
+ "MeshAgentRequest",
+ "publishRequest",
+ 4,
+ "MeshAgentPublishRequest",
+ ),
+];
+
+const ACTION_VARIANTS: &[RequiredVariant] = &[
+ variant("MeshAgentAction", "validateFrame", 0),
+ variant("MeshAgentAction", "stageDelivery", 1),
+ variant("MeshAgentAction", "observeEventHead", 2),
+ variant("MeshAgentAction", "status", 3),
+ variant("MeshAgentAction", "publish", 4),
+];
+
+const RESPONSE_FIELDS: &[RequiredField] = &[
+ field("MeshAgentResponse", "requestId", 0, "Text"),
+ field("MeshAgentResponse", "status", 1, "MeshAgentResponseStatus"),
+ field("MeshAgentResponse", "receipt", 2, "MeshAgentReceipt"),
+ field("MeshAgentResponse", "errors", 3, "List(MeshAgentError)"),
+ field(
+ "MeshAgentResponse",
+ "statusResponse",
+ 4,
+ "MeshAgentStatusResponse",
+ ),
+ field(
+ "MeshAgentResponse",
+ "publishResponse",
+ 5,
+ "MeshAgentPublishResponse",
+ ),
+];
+
+const RESPONSE_STATUS_VARIANTS: &[RequiredVariant] = &[
+ variant("MeshAgentResponseStatus", "accepted", 0),
+ variant("MeshAgentResponseStatus", "deferred", 1),
+ variant("MeshAgentResponseStatus", "rejected", 2),
+];
+
+const RECEIPT_FIELDS: &[RequiredField] = &[
+ field("MeshAgentReceipt", "frameDigest", 0, "Text"),
+ field("MeshAgentReceipt", "acceptedEventHeads", 1, "List(Text)"),
+];
+
+const STATUS_FIELDS: &[RequiredField] = &[
+ field("MeshAgentStatusRequest", "includeTransports", 0, "Bool"),
+ field(
+ "MeshAgentStatusResponse",
+ "readiness",
+ 0,
+ "MeshAgentReadinessState",
+ ),
+ field(
+ "MeshAgentStatusResponse",
+ "implementationState",
+ 1,
+ "MeshAgentImplementationState",
+ ),
+ field(
+ "MeshAgentStatusResponse",
+ "transports",
+ 2,
+ "List(MeshAgentTransportStatus)",
+ ),
+ field("MeshAgentTransportStatus", "transportKind", 0, "Text"),
+ field("MeshAgentTransportStatus", "profileId", 1, "Text"),
+ field("MeshAgentTransportStatus", "endpointUri", 2, "Text"),
+ field(
+ "MeshAgentTransportStatus",
+ "readiness",
+ 3,
+ "MeshAgentReadinessState",
+ ),
+ field(
+ "MeshAgentTransportStatus",
+ "implementationState",
+ 4,
+ "MeshAgentImplementationState",
+ ),
+ field("MeshAgentTransportStatus", "publishUsable", 5, "Bool"),
+ field("MeshAgentTransportStatus", "fetchUsable", 6, "Bool"),
+ field("MeshAgentTransportStatus", "redactedMessage", 7, "Text"),
+];
+
+const READINESS_VARIANTS: &[RequiredVariant] = &[
+ variant("MeshAgentReadinessState", "ready", 0),
+ variant("MeshAgentReadinessState", "disabled", 1),
+ variant("MeshAgentReadinessState", "misconfigured", 2),
+ variant("MeshAgentReadinessState", "previewUnavailable", 3),
+];
+
+const IMPLEMENTATION_VARIANTS: &[RequiredVariant] = &[
+ variant("MeshAgentImplementationState", "available", 0),
+ variant("MeshAgentImplementationState", "disabled", 1),
+ variant("MeshAgentImplementationState", "misconfigured", 2),
+ variant("MeshAgentImplementationState", "previewUnavailable", 3),
+];
+
+const PUBLISH_FIELDS: &[RequiredField] = &[
+ field("MeshAgentPublishRequest", "publishRequestId", 0, "Text"),
+ field("MeshAgentPublishRequest", "payloadCbor", 1, "Data"),
+ field("MeshAgentPublishResponse", "publishRequestId", 0, "Text"),
+ field(
+ "MeshAgentPublishResponse",
+ "status",
+ 1,
+ "MeshAgentResponseStatus",
+ ),
+ field(
+ "MeshAgentPublishResponse",
+ "transportReceipts",
+ 2,
+ "List(MeshAgentTransportReceipt)",
+ ),
+ field("MeshAgentTransportReceipt", "transportKind", 0, "Text"),
+ field("MeshAgentTransportReceipt", "endpointUri", 1, "Text"),
+ field("MeshAgentTransportReceipt", "deliveryStatus", 2, "Text"),
+ field("MeshAgentTransportReceipt", "redactedMessage", 3, "Text"),
+];
+
+const ERROR_FIELDS: &[RequiredField] = &[
+ field("MeshAgentError", "code", 0, "Text"),
+ field("MeshAgentError", "message", 1, "Text"),
+];
+
+const fn field(
+ owner: &'static str,
+ name: &'static str,
+ ordinal: u16,
+ field_type: &'static str,
+) -> RequiredField {
+ RequiredField {
+ owner,
+ name,
+ ordinal,
+ field_type,
+ }
+}
+
+const fn variant(owner: &'static str, name: &'static str, ordinal: u16) -> RequiredVariant {
+ RequiredVariant {
+ owner,
+ name,
+ ordinal,
+ }
+}
+
+pub(crate) fn validate_schema_text(schema: &str) -> Result<(), RadrootsMeshAgentProtoError> {
+ let parsed = parse_schema(schema)?;
+ if parsed.schema_id.as_deref() != Some(RADROOTS_MESH_AGENT_SCHEMA_ID) {
+ return Err(RadrootsMeshAgentProtoError::MissingSchemaId);
+ }
+ if parsed.namespace.as_deref() != Some(RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE) {
+ return Err(RadrootsMeshAgentProtoError::MissingNamespace);
+ }
+ validate_fields(
+ &parsed,
+ REQUEST_FIELDS,
+ RadrootsMeshAgentProtoError::MissingRequest,
+ )?;
+ validate_variants(
+ &parsed,
+ ACTION_VARIANTS,
+ RadrootsMeshAgentProtoError::MissingAction,
+ )?;
+ validate_fields(
+ &parsed,
+ RESPONSE_FIELDS,
+ RadrootsMeshAgentProtoError::MissingResponse,
+ )?;
+ validate_variants(
+ &parsed,
+ RESPONSE_STATUS_VARIANTS,
+ RadrootsMeshAgentProtoError::MissingResponse,
+ )?;
+ validate_fields(
+ &parsed,
+ RECEIPT_FIELDS,
+ RadrootsMeshAgentProtoError::MissingReceipt,
+ )?;
+ validate_fields(
+ &parsed,
+ STATUS_FIELDS,
+ RadrootsMeshAgentProtoError::MissingStatusSurface,
+ )?;
+ validate_variants(
+ &parsed,
+ READINESS_VARIANTS,
+ RadrootsMeshAgentProtoError::MissingStatusSurface,
+ )?;
+ validate_variants(
+ &parsed,
+ IMPLEMENTATION_VARIANTS,
+ RadrootsMeshAgentProtoError::MissingStatusSurface,
+ )?;
+ validate_fields(
+ &parsed,
+ PUBLISH_FIELDS,
+ RadrootsMeshAgentProtoError::MissingPublishSurface,
+ )?;
+ validate_fields(
+ &parsed,
+ ERROR_FIELDS,
+ RadrootsMeshAgentProtoError::MissingError,
+ )
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum SchemaToken {
+ Ident(String),
+ Number(String),
+ StringLiteral(String),
+ Symbol(char),
+}
+
+#[derive(Default)]
+struct SchemaAst {
+ schema_id: Option<String>,
+ namespace: Option<String>,
+ structs: BTreeMap<String, StructDecl>,
+ enums: BTreeMap<String, EnumDecl>,
+}
+
+struct StructDecl {
+ fields: BTreeMap<String, FieldDecl>,
+}
+
+struct FieldDecl {
+ ordinal: u16,
+ field_type: String,
+}
+
+struct EnumDecl {
+ variants: BTreeMap<String, u16>,
+}
+
+struct SchemaParser {
+ tokens: Vec<SchemaToken>,
+ index: usize,
+}
+
+fn parse_schema(schema: &str) -> Result<SchemaAst, RadrootsMeshAgentProtoError> {
+ let tokens = lex_schema(schema)?;
+ let mut parser = SchemaParser { tokens, index: 0 };
+ parser.parse()
+}
+
+fn lex_schema(schema: &str) -> Result<Vec<SchemaToken>, RadrootsMeshAgentProtoError> {
+ let chars: Vec<char> = schema.chars().collect();
+ let mut tokens = Vec::new();
+ let mut index = 0;
+ while index < chars.len() {
+ let ch = chars[index];
+ if ch.is_whitespace() {
+ index += 1;
+ continue;
+ }
+ if ch == '#' {
+ index = skip_line_comment(&chars, index + 1);
+ continue;
+ }
+ if ch == '/' && chars.get(index + 1) == Some(&'/') {
+ index = skip_line_comment(&chars, index + 2);
+ continue;
+ }
+ if ch == '/' && chars.get(index + 1) == Some(&'*') {
+ index = skip_block_comment(&chars, index + 2)?;
+ continue;
+ }
+ if is_ident_start(ch) {
+ let start = index;
+ index += 1;
+ while index < chars.len() && is_ident_continue(chars[index]) {
+ index += 1;
+ }
+ tokens.push(SchemaToken::Ident(chars[start..index].iter().collect()));
+ continue;
+ }
+ if ch.is_ascii_digit() {
+ let start = index;
+ index += 1;
+ while index < chars.len() && is_number_continue(chars[index]) {
+ index += 1;
+ }
+ tokens.push(SchemaToken::Number(chars[start..index].iter().collect()));
+ continue;
+ }
+ if ch == '"' {
+ let (literal, next_index) = read_string_literal(&chars, index + 1)?;
+ tokens.push(SchemaToken::StringLiteral(literal));
+ index = next_index;
+ continue;
+ }
+ if is_schema_symbol(ch) {
+ tokens.push(SchemaToken::Symbol(ch));
+ index += 1;
+ continue;
+ }
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ Ok(tokens)
+}
+
+fn skip_line_comment(chars: &[char], mut index: usize) -> usize {
+ while index < chars.len() && chars[index] != '\n' {
+ index += 1;
+ }
+ index
+}
+
+fn skip_block_comment(
+ chars: &[char],
+ mut index: usize,
+) -> Result<usize, RadrootsMeshAgentProtoError> {
+ while index + 1 < chars.len() {
+ if chars[index] == '*' && chars[index + 1] == '/' {
+ return Ok(index + 2);
+ }
+ index += 1;
+ }
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+}
+
+fn read_string_literal(
+ chars: &[char],
+ mut index: usize,
+) -> Result<(String, usize), RadrootsMeshAgentProtoError> {
+ let mut literal = String::new();
+ while index < chars.len() {
+ match chars[index] {
+ '"' => return Ok((literal, index + 1)),
+ '\\' => {
+ index += 1;
+ if index >= chars.len() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ literal.push(chars[index]);
+ }
+ ch => literal.push(ch),
+ }
+ index += 1;
+ }
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+}
+
+fn is_ident_start(ch: char) -> bool {
+ ch.is_ascii_alphabetic() || ch == '_'
+}
+
+fn is_ident_continue(ch: char) -> bool {
+ ch.is_ascii_alphanumeric() || ch == '_'
+}
+
+fn is_number_continue(ch: char) -> bool {
+ ch.is_ascii_hexdigit() || ch == 'x' || ch == 'X'
+}
+
+fn is_schema_symbol(ch: char) -> bool {
+ matches!(
+ ch,
+ '@' | '{' | '}' | ';' | ':' | '(' | ')' | '.' | '$' | ',' | '='
+ )
+}
+
+impl SchemaParser {
+ fn parse(&mut self) -> Result<SchemaAst, RadrootsMeshAgentProtoError> {
+ let mut ast = SchemaAst::default();
+ while !self.is_done() {
+ match self.peek() {
+ Some(SchemaToken::Symbol('@')) => self.parse_schema_id(&mut ast)?,
+ Some(SchemaToken::Symbol('$')) => self.parse_annotation(&mut ast)?,
+ Some(SchemaToken::Ident(value)) if value == "using" => self.skip_statement()?,
+ Some(SchemaToken::Ident(value)) if value == "struct" => {
+ let (name, decl) = self.parse_struct()?;
+ if ast.structs.insert(name, decl).is_some() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ }
+ Some(SchemaToken::Ident(value)) if value == "enum" => {
+ let (name, decl) = self.parse_enum()?;
+ if ast.enums.insert(name, decl).is_some() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ }
+ Some(SchemaToken::Symbol(';')) => {
+ self.index += 1;
+ }
+ _ => return Err(RadrootsMeshAgentProtoError::InvalidSchema),
+ }
+ }
+ Ok(ast)
+ }
+
+ fn parse_schema_id(&mut self, ast: &mut SchemaAst) -> Result<(), RadrootsMeshAgentProtoError> {
+ self.expect_symbol('@')?;
+ let schema_id = self.expect_number()?;
+ self.expect_symbol(';')?;
+ if ast.schema_id.replace(schema_id).is_some() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ Ok(())
+ }
+
+ fn parse_annotation(&mut self, ast: &mut SchemaAst) -> Result<(), RadrootsMeshAgentProtoError> {
+ self.expect_symbol('$')?;
+ let is_namespace = self.consume_ident("Cxx")
+ && self.consume_symbol('.')
+ && self.consume_ident("namespace")
+ && self.consume_symbol('(');
+ if is_namespace {
+ let namespace = self.expect_string_literal()?;
+ self.expect_symbol(')')?;
+ self.expect_symbol(';')?;
+ if ast.namespace.replace(namespace).is_some() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ return Ok(());
+ }
+ self.skip_statement()
+ }
+
+ fn parse_struct(&mut self) -> Result<(String, StructDecl), RadrootsMeshAgentProtoError> {
+ self.expect_ident_value("struct")?;
+ let name = self.expect_ident()?;
+ self.expect_symbol('{')?;
+ let mut fields = BTreeMap::new();
+ let mut ordinals = BTreeSet::new();
+ while !self.consume_symbol('}') {
+ let field_name = self.expect_ident()?;
+ self.expect_symbol('@')?;
+ let ordinal = parse_ordinal(&self.expect_number()?)?;
+ self.expect_symbol(':')?;
+ let field_type = self.parse_type()?;
+ if !ordinals.insert(ordinal)
+ || fields
+ .insert(
+ field_name,
+ FieldDecl {
+ ordinal,
+ field_type,
+ },
+ )
+ .is_some()
+ {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ }
+ Ok((name, StructDecl { fields }))
+ }
+
+ fn parse_enum(&mut self) -> Result<(String, EnumDecl), RadrootsMeshAgentProtoError> {
+ self.expect_ident_value("enum")?;
+ let name = self.expect_ident()?;
+ self.expect_symbol('{')?;
+ let mut variants = BTreeMap::new();
+ let mut ordinals = BTreeSet::new();
+ while !self.consume_symbol('}') {
+ let variant_name = self.expect_ident()?;
+ self.expect_symbol('@')?;
+ let ordinal = parse_ordinal(&self.expect_number()?)?;
+ self.expect_symbol(';')?;
+ if !ordinals.insert(ordinal) || variants.insert(variant_name, ordinal).is_some() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ }
+ Ok((name, EnumDecl { variants }))
+ }
+
+ fn parse_type(&mut self) -> Result<String, RadrootsMeshAgentProtoError> {
+ let mut field_type = String::new();
+ while let Some(token) = self.peek() {
+ if token == &SchemaToken::Symbol(';') {
+ self.index += 1;
+ if field_type.is_empty() {
+ return Err(RadrootsMeshAgentProtoError::InvalidSchema);
+ }
+ return Ok(field_type);
+ }
+ append_token_text(&mut field_type, token)?;
+ self.index += 1;
+ }
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+ }
+
+ fn skip_statement(&mut self) -> Result<(), RadrootsMeshAgentProtoError> {
+ while !self.is_done() {
+ if self.consume_symbol(';') {
+ return Ok(());
+ }
+ self.index += 1;
+ }
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+ }
+
+ fn peek(&self) -> Option<&SchemaToken> {
+ self.tokens.get(self.index)
+ }
+
+ fn is_done(&self) -> bool {
+ self.index >= self.tokens.len()
+ }
+
+ fn consume_symbol(&mut self, symbol: char) -> bool {
+ if self.peek() == Some(&SchemaToken::Symbol(symbol)) {
+ self.index += 1;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn consume_ident(&mut self, expected: &str) -> bool {
+ match self.peek() {
+ Some(SchemaToken::Ident(value)) if value == expected => {
+ self.index += 1;
+ true
+ }
+ _ => false,
+ }
+ }
+
+ fn expect_symbol(&mut self, expected: char) -> Result<(), RadrootsMeshAgentProtoError> {
+ if self.consume_symbol(expected) {
+ Ok(())
+ } else {
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+ }
+ }
+
+ fn expect_ident_value(&mut self, expected: &str) -> Result<(), RadrootsMeshAgentProtoError> {
+ if self.consume_ident(expected) {
+ Ok(())
+ } else {
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+ }
+ }
+
+ fn expect_ident(&mut self) -> Result<String, RadrootsMeshAgentProtoError> {
+ match self.peek().cloned() {
+ Some(SchemaToken::Ident(value)) => {
+ self.index += 1;
+ Ok(value)
+ }
+ _ => Err(RadrootsMeshAgentProtoError::InvalidSchema),
+ }
+ }
+
+ fn expect_number(&mut self) -> Result<String, RadrootsMeshAgentProtoError> {
+ match self.peek().cloned() {
+ Some(SchemaToken::Number(value)) => {
+ self.index += 1;
+ Ok(value)
+ }
+ _ => Err(RadrootsMeshAgentProtoError::InvalidSchema),
+ }
+ }
+
+ fn expect_string_literal(&mut self) -> Result<String, RadrootsMeshAgentProtoError> {
+ match self.peek().cloned() {
+ Some(SchemaToken::StringLiteral(value)) => {
+ self.index += 1;
+ Ok(value)
+ }
+ _ => Err(RadrootsMeshAgentProtoError::InvalidSchema),
+ }
+ }
+}
+
+fn append_token_text(
+ output: &mut String,
+ token: &SchemaToken,
+) -> Result<(), RadrootsMeshAgentProtoError> {
+ match token {
+ SchemaToken::Ident(value) | SchemaToken::Number(value) => {
+ output.push_str(value.as_str());
+ Ok(())
+ }
+ SchemaToken::Symbol(symbol) if matches!(symbol, '(' | ')' | '.') => {
+ output.push(*symbol);
+ Ok(())
+ }
+ _ => Err(RadrootsMeshAgentProtoError::InvalidSchema),
+ }
+}
+
+fn parse_ordinal(value: &str) -> Result<u16, RadrootsMeshAgentProtoError> {
+ value
+ .parse::<u16>()
+ .map_err(|_| RadrootsMeshAgentProtoError::InvalidSchema)
+}
+
+fn validate_fields(
+ ast: &SchemaAst,
+ fields: &[RequiredField],
+ error: RadrootsMeshAgentProtoError,
+) -> Result<(), RadrootsMeshAgentProtoError> {
+ for required in fields {
+ let decl = ast
+ .structs
+ .get(required.owner)
+ .ok_or_else(|| error.clone())?;
+ let field_matches = decl.fields.get(required.name).is_some_and(|field| {
+ field.ordinal == required.ordinal && field.field_type == required.field_type
+ });
+ if !field_matches {
+ return Err(error.clone());
+ }
+ }
+ Ok(())
+}
+
+fn validate_variants(
+ ast: &SchemaAst,
+ variants: &[RequiredVariant],
+ error: RadrootsMeshAgentProtoError,
+) -> Result<(), RadrootsMeshAgentProtoError> {
+ for required in variants {
+ let decl = ast.enums.get(required.owner).ok_or_else(|| error.clone())?;
+ let variant_matches = decl
+ .variants
+ .get(required.name)
+ .is_some_and(|ordinal| *ordinal == required.ordinal);
+ if !variant_matches {
+ return Err(error.clone());
+ }
+ }
+ Ok(())
+}
diff --git a/crates/mesh_agent_proto/tests/schema.rs b/crates/mesh_agent_proto/tests/schema.rs
@@ -152,9 +152,82 @@ fn schema_validator_reports_each_missing_required_surface() {
}
#[test]
+fn schema_validator_rejects_commented_required_declarations() {
+ let schema =
+ RADROOTS_MESH_AGENT_SCHEMA.replace(" frameCbor @2 :Data;", " # frameCbor @2 :Data;");
+
+ assert_eq!(
+ validate_schema_text(schema.as_str()),
+ Err(RadrootsMeshAgentProtoError::MissingRequest)
+ );
+}
+
+#[test]
+fn schema_validator_rejects_misplaced_required_declarations() {
+ let schema = RADROOTS_MESH_AGENT_SCHEMA
+ .replace(
+ " acceptedEventHeads @1 :List(Text);",
+ " acceptedEventIds @1 :List(Text);",
+ )
+ .replace(
+ " message @1 :Text;",
+ " message @1 :Text;\n acceptedEventHeads @2 :List(Text);",
+ );
+
+ assert_eq!(
+ validate_schema_text(schema.as_str()),
+ Err(RadrootsMeshAgentProtoError::MissingReceipt)
+ );
+}
+
+#[test]
+fn schema_validator_rejects_duplicate_incompatible_declarations() {
+ let duplicate_ordinal = RADROOTS_MESH_AGENT_SCHEMA.replace(
+ " frameCbor @2 :Data;",
+ " frameCbor @2 :Data;\n frameBytes @2 :Data;",
+ );
+ let duplicate_name = RADROOTS_MESH_AGENT_SCHEMA.replace(
+ " frameCbor @2 :Data;",
+ " frameCbor @2 :Data;\n frameCbor @9 :Text;",
+ );
+
+ assert_eq!(
+ validate_schema_text(duplicate_ordinal.as_str()),
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+ );
+ assert_eq!(
+ validate_schema_text(duplicate_name.as_str()),
+ Err(RadrootsMeshAgentProtoError::InvalidSchema)
+ );
+}
+
+#[test]
+fn schema_validator_rejects_type_drift() {
+ let request_type_drift =
+ RADROOTS_MESH_AGENT_SCHEMA.replace(" frameCbor @2 :Data;", " frameCbor @2 :Text;");
+ let status_type_drift = RADROOTS_MESH_AGENT_SCHEMA.replace(
+ " includeTransports @0 :Bool;",
+ " includeTransports @0 :Text;",
+ );
+
+ assert_eq!(
+ validate_schema_text(request_type_drift.as_str()),
+ Err(RadrootsMeshAgentProtoError::MissingRequest)
+ );
+ assert_eq!(
+ validate_schema_text(status_type_drift.as_str()),
+ Err(RadrootsMeshAgentProtoError::MissingStatusSurface)
+ );
+}
+
+#[test]
fn mesh_agent_proto_errors_have_stable_display_strings() {
let cases = [
(
+ RadrootsMeshAgentProtoError::InvalidSchema,
+ "mesh agent schema is invalid",
+ ),
+ (
RadrootsMeshAgentProtoError::MissingSchemaId,
"mesh agent schema id is missing",
),