Skip to main content

lean_ctx_protocol/
engine_context_source_execution_v2.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Explicit v2 delivery of exact signed bytes; wire joins do not establish key trust.
4
5use 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
13/// The existing canonical receipt reader's byte limit, applied before decoding.
14pub const MAX_ENGINE_SOURCE_RECEIPT_DOCUMENT_BYTES: usize = 1024 * 1024;
15/// Total JSON transport bound, including escaping of the exact UTF-8 document.
16pub const MAX_ENGINE_SOURCE_EXECUTION_V2_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
17
18/// Opt-in v2 response: the unchanged v1 execution and exact persisted canonical JSON.
19///
20/// `receipt_document_json.as_bytes()` are the signed document bytes, not a
21/// re-rendered object. Consumers must independently admit the signer and verify
22/// the Ed25519 signature; neither a digest nor this validator grants trust.
23#[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    /// Validate byte identity and signed-lineage joins, not signer admission.
33    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    /// Retain every v1 request/source/task join before admitting the document.
93    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}