lean_ctx_protocol/
engine_context_source_execution_v2.rs1use serde::{Deserialize, Serialize};
6
7use crate::engine_context_source_execution::{canonical_sha256, sha256_digest};
8use crate::{
9 AcceptanceState, EngineContextSourceExecutionRequestV1, EngineContextSourceExecutionResponseV1,
10 ReceiptDocumentV1, ReceiptEvidenceKindV1, ReceiptTerminalStatusV1, ValidationError,
11};
12
13pub const MAX_ENGINE_SOURCE_RECEIPT_DOCUMENT_BYTES: usize = 1024 * 1024;
15pub const MAX_ENGINE_SOURCE_EXECUTION_V2_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct EngineContextSourceExecutionResponseV2 {
26 pub schema_version: u32,
27 pub execution: EngineContextSourceExecutionResponseV1,
28 pub receipt_document_json: String,
29}
30
31impl EngineContextSourceExecutionResponseV2 {
32 pub fn validate(&self) -> Result<(), ValidationError> {
34 if self.schema_version != 2
35 || self.receipt_document_json.len() > MAX_ENGINE_SOURCE_RECEIPT_DOCUMENT_BYTES
36 {
37 return Err(invalid());
38 }
39 self.execution.validate()?;
40 let bytes = self.receipt_document_json.as_bytes();
41 let projection = &self.execution.canonical_receipt;
42 if sha256_digest(bytes)? != projection.receipt_digest {
43 return Err(invalid());
44 }
45 let document = ReceiptDocumentV1::from_canonical_bytes(bytes)?;
46 let lineage = &document.lineage;
47 let plan = &self.execution.execution_plan;
48 let invocation = &self.execution.invocation;
49 if document.receipt_id.as_str() != projection.receipt_id.as_str()
50 || document.outcome.state != AcceptanceState::Unknown
51 || document.status != ReceiptTerminalStatusV1::Succeeded
52 || lineage.task_id != plan.task_id
53 || lineage.plan_id != plan.plan_id
54 || lineage.plan_ref != canonical_sha256(plan)?
55 || lineage.invocation_id != invocation.invocation_id.as_str()
56 || lineage.invocation_ref != canonical_sha256(invocation)?
57 || !invocation.source_refs.iter().any(|reference| {
58 reference.as_str() == format!("task:{}", lineage.task_ref.as_str())
59 })
60 {
61 return Err(invalid());
62 }
63 let native = self
64 .execution
65 .observation
66 .receipt_link
67 .as_ref()
68 .ok_or_else(invalid)?;
69 let observation_digest = canonical_sha256(&self.execution.observation)?;
70 for (uri, digest) in [
71 (
72 format!("artifact://engine/receipts/{}", native.receipt_digest.hex()),
73 &native.receipt_digest,
74 ),
75 ("artifact://engine/observation".into(), &observation_digest),
76 ] {
77 if !document.evidence_refs.iter().any(|evidence| {
78 evidence.uri.as_str() == uri
79 && &evidence.digest == digest
80 && evidence.kind == ReceiptEvidenceKindV1::Measurement
81 }) {
82 return Err(invalid());
83 }
84 }
85 let encoded = serde_json::to_vec(self).map_err(|_| invalid())?;
86 if encoded.len() > MAX_ENGINE_SOURCE_EXECUTION_V2_RESPONSE_BYTES {
87 return Err(invalid());
88 }
89 Ok(())
90 }
91
92 pub fn validate_against(
94 &self,
95 request: &EngineContextSourceExecutionRequestV1,
96 ) -> Result<(), ValidationError> {
97 self.execution.validate_against(request)?;
98 self.validate()
99 }
100}
101
102fn invalid() -> ValidationError {
103 ValidationError::new("source execution v2 does not bind the exact canonical receipt")
104}