@@ -8,6 +8,7 @@ use rkyv::{Archive, Deserialize, Serialize};
88use zerocopy:: { AsBytes , FromBytes , FromZeroes } ;
99
1010use crate :: hlc:: HlcTimestamp ;
11+ use crate :: provenance:: ProvenanceHeader ;
1112
1213/// Unique message identifier.
1314#[ derive(
@@ -372,12 +373,28 @@ pub trait RingMessage: Send + Sync + 'static {
372373}
373374
374375/// Envelope containing header and serialized payload.
376+ ///
377+ /// The optional `provenance` slot carries PROV-O attribution metadata for
378+ /// NSAI reasoning chains (see [`crate::provenance`]). When `None`, the field
379+ /// is a single discriminant byte - zero cost for the common case. When
380+ /// populated, it adds a fixed-size [`ProvenanceHeader`] (see that type for
381+ /// size details).
382+ ///
383+ /// The `provenance` field is *not* included in the legacy
384+ /// [`MessageEnvelope::to_bytes`] / [`MessageEnvelope::from_bytes`] wire format,
385+ /// which is defined by `MessageHeader` + raw payload for backwards
386+ /// compatibility. Provenance travels separately (e.g. as part of rkyv-encoded
387+ /// envelope transfer on GPU) or is reattached by the router.
375388#[ derive( Debug , Clone ) ]
376389pub struct MessageEnvelope {
377390 /// Message header.
378391 pub header : MessageHeader ,
379392 /// Serialized payload.
380393 pub payload : Vec < u8 > ,
394+ /// Optional PROV-O attribution metadata. Defaults to `None`; only
395+ /// populated when the message participates in an audited reasoning
396+ /// chain (e.g. VynGraph NSAI pipelines).
397+ pub provenance : Option < ProvenanceHeader > ,
381398}
382399
383400impl MessageEnvelope {
@@ -399,7 +416,11 @@ impl MessageEnvelope {
399416 . with_correlation ( message. correlation_id ( ) )
400417 . with_priority ( message. priority ( ) ) ;
401418
402- Self { header, payload }
419+ Self {
420+ header,
421+ payload,
422+ provenance : None ,
423+ }
403424 }
404425
405426 /// Get total size (header + payload).
@@ -408,6 +429,10 @@ impl MessageEnvelope {
408429 }
409430
410431 /// Serialize to contiguous bytes.
432+ ///
433+ /// NOTE: the provenance metadata is intentionally *not* serialised here.
434+ /// This method keeps the historical wire format unchanged; provenance is
435+ /// transported out-of-band or via rkyv-encoded transfer.
411436 pub fn to_bytes ( & self ) -> Vec < u8 > {
412437 let mut bytes = Vec :: with_capacity ( self . total_size ( ) ) ;
413438 bytes. extend_from_slice ( self . header . as_bytes ( ) ) ;
@@ -416,6 +441,9 @@ impl MessageEnvelope {
416441 }
417442
418443 /// Deserialize from bytes.
444+ ///
445+ /// Reconstructs an envelope with `provenance: None`. Callers that need
446+ /// provenance must reattach it via [`MessageEnvelope::with_provenance`].
419447 pub fn from_bytes ( bytes : & [ u8 ] ) -> crate :: error:: Result < Self > {
420448 if bytes. len ( ) < std:: mem:: size_of :: < MessageHeader > ( ) {
421449 return Err ( crate :: error:: RingKernelError :: DeserializationError (
@@ -445,7 +473,11 @@ impl MessageEnvelope {
445473
446474 let payload = bytes[ payload_start..payload_end] . to_vec ( ) ;
447475
448- Ok ( Self { header, payload } )
476+ Ok ( Self {
477+ header,
478+ payload,
479+ provenance : None ,
480+ } )
449481 }
450482
451483 /// Create an empty envelope (for testing).
@@ -454,8 +486,22 @@ impl MessageEnvelope {
454486 Self {
455487 header,
456488 payload : Vec :: new ( ) ,
489+ provenance : None ,
457490 }
458491 }
492+
493+ /// Attach a PROV-O provenance header (builder-style).
494+ pub fn with_provenance ( mut self , provenance : ProvenanceHeader ) -> Self {
495+ self . provenance = Some ( provenance) ;
496+ self
497+ }
498+
499+ /// Strip provenance (builder-style). Useful when routing a message into
500+ /// an untrusted tenant boundary where attribution must not leak.
501+ pub fn without_provenance ( mut self ) -> Self {
502+ self . provenance = None ;
503+ self
504+ }
459505}
460506
461507#[ cfg( test) ]
@@ -499,12 +545,39 @@ mod tests {
499545 let envelope = MessageEnvelope {
500546 header,
501547 payload : vec ! [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ] ,
548+ provenance : None ,
502549 } ;
503550
504551 let bytes = envelope. to_bytes ( ) ;
505552 let restored = MessageEnvelope :: from_bytes ( & bytes) . unwrap ( ) ;
506553
507554 assert_eq ! ( envelope. header. message_type, restored. header. message_type) ;
508555 assert_eq ! ( envelope. payload, restored. payload) ;
556+ // Provenance is intentionally not round-tripped through the legacy
557+ // wire format; restored envelopes carry `None` until reattached.
558+ assert ! ( restored. provenance. is_none( ) ) ;
559+ }
560+
561+ #[ test]
562+ fn test_envelope_with_provenance ( ) {
563+ use crate :: provenance:: { ProvNodeType , ProvRelationKind , ProvenanceBuilder } ;
564+
565+ let hdr = ProvenanceBuilder :: new ( ProvNodeType :: Entity , 0x42 )
566+ . with_relation ( ProvRelationKind :: WasAttributedTo , 0x7 )
567+ . build ( )
568+ . unwrap ( ) ;
569+
570+ let envelope = MessageEnvelope :: empty ( 0 , 1 , HlcTimestamp :: now ( 1 ) ) . with_provenance ( hdr) ;
571+ assert ! ( envelope. provenance. is_some( ) ) ;
572+ assert_eq ! ( envelope. provenance. unwrap( ) . node_id, 0x42 ) ;
573+
574+ let stripped = envelope. without_provenance ( ) ;
575+ assert ! ( stripped. provenance. is_none( ) ) ;
576+ }
577+
578+ #[ test]
579+ fn test_envelope_default_has_no_provenance ( ) {
580+ let envelope = MessageEnvelope :: empty ( 0 , 1 , HlcTimestamp :: zero ( ) ) ;
581+ assert ! ( envelope. provenance. is_none( ) ) ;
509582 }
510583}
0 commit comments