mpl_core/
envelope.rs

1//! MPL Envelope
2//!
3//! The semantic wrapper around payloads transmitted over MCP/A2A.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::hash::semantic_hash;
10use crate::qom::QomReport;
11use crate::stype::SType;
12
13/// MPL Envelope - the core message wrapper
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct MplEnvelope {
16    /// Unique message identifier
17    pub id: String,
18
19    /// Semantic type of the payload
20    pub stype: String,
21
22    /// The actual payload data
23    pub payload: serde_json::Value,
24
25    /// Semantic type for arguments (for tool calls)
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub args_stype: Option<String>,
28
29    /// QoM profile used for validation
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub profile: Option<String>,
32
33    /// Semantic hash of the canonical payload
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub sem_hash: Option<String>,
36
37    /// Provenance metadata
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub provenance: Option<Provenance>,
40
41    /// QoM evaluation report (typically on responses)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub qom_report: Option<QomReport>,
44
45    /// Optional feature flags
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub features: Vec<String>,
48
49    /// Timestamp
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub timestamp: Option<DateTime<Utc>>,
52}
53
54impl MplEnvelope {
55    /// Create a new envelope with a random ID
56    pub fn new(stype: impl Into<String>, payload: serde_json::Value) -> Self {
57        Self {
58            id: Uuid::new_v4().to_string(),
59            stype: stype.into(),
60            payload,
61            args_stype: None,
62            profile: None,
63            sem_hash: None,
64            provenance: None,
65            qom_report: None,
66            features: Vec::new(),
67            timestamp: Some(Utc::now()),
68        }
69    }
70
71    /// Create an envelope from an SType
72    pub fn from_stype(stype: &SType, payload: serde_json::Value) -> Self {
73        Self::new(stype.id(), payload)
74    }
75
76    /// Set the args SType (for tool calls)
77    pub fn with_args_stype(mut self, args_stype: impl Into<String>) -> Self {
78        self.args_stype = Some(args_stype.into());
79        self
80    }
81
82    /// Set the QoM profile
83    pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
84        self.profile = Some(profile.into());
85        self
86    }
87
88    /// Set provenance metadata
89    pub fn with_provenance(mut self, provenance: Provenance) -> Self {
90        self.provenance = Some(provenance);
91        self
92    }
93
94    /// Add feature flags
95    pub fn with_features(mut self, features: Vec<String>) -> Self {
96        self.features = features;
97        self
98    }
99
100    /// Compute and set the semantic hash
101    pub fn compute_hash(&mut self) -> crate::error::Result<()> {
102        self.sem_hash = Some(semantic_hash(&self.payload)?);
103        Ok(())
104    }
105
106    /// Verify the semantic hash matches the payload
107    pub fn verify_hash(&self) -> crate::error::Result<bool> {
108        match &self.sem_hash {
109            Some(expected) => {
110                let actual = semantic_hash(&self.payload)?;
111                Ok(&actual == expected)
112            }
113            None => Ok(true), // No hash to verify
114        }
115    }
116
117    /// Attach a QoM report
118    pub fn with_qom_report(mut self, report: QomReport) -> Self {
119        self.qom_report = Some(report);
120        self
121    }
122
123    /// Parse the SType field into a structured SType
124    pub fn parsed_stype(&self) -> crate::error::Result<SType> {
125        SType::parse(&self.stype)
126    }
127}
128
129/// Provenance metadata tracking origin and transformation chain
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct Provenance {
132    /// Intent reference (SType or action identifier)
133    pub intent: String,
134
135    /// References to input context
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub inputs_ref: Vec<String>,
138
139    /// Consent receipt reference
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub consent_ref: Option<String>,
142
143    /// Policy reference
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub policy_ref: Option<String>,
146
147    /// Agent/model that produced this payload
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub agent: Option<String>,
150
151    /// Parent envelope ID (for tracing)
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub parent_id: Option<String>,
154
155    /// Optional signature over semantic hash
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub signature: Option<String>,
158
159    /// Artifacts/sources for groundedness checks
160    #[serde(default, skip_serializing_if = "Vec::is_empty")]
161    pub artifacts: Vec<Artifact>,
162}
163
164impl Provenance {
165    /// Create new provenance with an intent
166    pub fn new(intent: impl Into<String>) -> Self {
167        Self {
168            intent: intent.into(),
169            inputs_ref: Vec::new(),
170            consent_ref: None,
171            policy_ref: None,
172            agent: None,
173            parent_id: None,
174            signature: None,
175            artifacts: Vec::new(),
176        }
177    }
178
179    /// Add input references
180    pub fn with_inputs(mut self, inputs: Vec<String>) -> Self {
181        self.inputs_ref = inputs;
182        self
183    }
184
185    /// Set consent reference
186    pub fn with_consent(mut self, consent_ref: impl Into<String>) -> Self {
187        self.consent_ref = Some(consent_ref.into());
188        self
189    }
190
191    /// Set policy reference
192    pub fn with_policy(mut self, policy_ref: impl Into<String>) -> Self {
193        self.policy_ref = Some(policy_ref.into());
194        self
195    }
196
197    /// Set agent identifier
198    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
199        self.agent = Some(agent.into());
200        self
201    }
202}
203
204/// Artifact for provenance (citations, sources)
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct Artifact {
207    /// Reference identifier
208    #[serde(rename = "ref")]
209    pub reference: String,
210
211    /// Type of artifact (document, api, database, etc.)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub artifact_type: Option<String>,
214
215    /// Content or excerpt
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub text: Option<String>,
218
219    /// URL if applicable
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub url: Option<String>,
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use serde_json::json;
228
229    #[test]
230    fn test_envelope_creation() {
231        let envelope = MplEnvelope::new(
232            "org.calendar.Event.v1",
233            json!({
234                "title": "Meeting",
235                "start": "2025-01-01T10:00:00Z"
236            }),
237        );
238
239        assert!(!envelope.id.is_empty());
240        assert_eq!(envelope.stype, "org.calendar.Event.v1");
241        assert!(envelope.timestamp.is_some());
242    }
243
244    #[test]
245    fn test_envelope_with_hash() {
246        let mut envelope = MplEnvelope::new("org.test.Test.v1", json!({"key": "value"}));
247        envelope.compute_hash().unwrap();
248
249        assert!(envelope.sem_hash.is_some());
250        assert!(envelope.sem_hash.as_ref().unwrap().starts_with("b3:"));
251        assert!(envelope.verify_hash().unwrap());
252    }
253
254    #[test]
255    fn test_envelope_serialization() {
256        let envelope = MplEnvelope::new("org.test.Test.v1", json!({"test": true}))
257            .with_profile("qom-basic")
258            .with_provenance(Provenance::new("test.action.v1"));
259
260        let json = serde_json::to_string(&envelope).unwrap();
261        let deserialized: MplEnvelope = serde_json::from_str(&json).unwrap();
262
263        assert_eq!(deserialized.stype, envelope.stype);
264        assert_eq!(deserialized.profile, envelope.profile);
265    }
266}