Skip to main content

lean_ctx_protocol/
engine_context_materialization.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Bounded materialization of an already-planned explicit source batch.
4//!
5//! The request carries the original source bodies because the planning
6//! response intentionally contains descriptors only.  The binding digest is
7//! an integrity join to that prior response, not an authorization grant or a
8//! receipt.
9
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use std::fmt::Write as _;
13
14use crate::{
15    EngineContextSourcePlanRequestV1, EngineContextSourcePlanResponseV1, SemanticVersion,
16    Sha256Digest, UtcTimestamp, ValidationError,
17};
18
19/// Keep the materialized context within the existing explicit-source bound.
20pub const MAX_ENGINE_SOURCE_MATERIALIZED_CONTEXT_BYTES: usize =
21    super::MAX_ENGINE_SOURCE_PLAN_REQUEST_BYTES;
22
23/// Re-submit the bounded original source batch with the digest of the plan to
24/// which the caller intends to bind materialization.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct EngineContextSourceMaterializationRequestV1 {
28    pub source_plan: EngineContextSourcePlanRequestV1,
29    pub expected_binding_digest: Sha256Digest,
30    /// Server-emitted identity time from a retention-enabled source plan.
31    /// This is an unsigned replay hint, never authorization or provenance.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub planning_evaluation_time: Option<UtcTimestamp>,
34}
35
36impl EngineContextSourceMaterializationRequestV1 {
37    /// Validate the original source request and its serialized size before any
38    /// policy planning or body joining occurs.
39    pub fn validate_payload(&self) -> Result<(), ValidationError> {
40        self.source_plan.validate_payload()?;
41        let encoded = serde_json::to_vec(self)
42            .map_err(|_| ValidationError::new("invalid Engine materialization request"))?;
43        if encoded.len() > super::MAX_ENGINE_SOURCE_PLAN_REQUEST_BYTES {
44            return Err(ValidationError::new(
45                "Engine materialization source request exceeds size bound",
46            ));
47        }
48        Ok(())
49    }
50}
51
52/// Digest-bound context bytes selected by the canonical source planner.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct EngineContextSourceMaterializationResponseV1 {
56    pub schema_version: u32,
57    pub transport_version: u32,
58    pub engine_interface_version: SemanticVersion,
59    pub plan: EngineContextSourcePlanResponseV1,
60    pub materialized_digest: Sha256Digest,
61    pub materialized_token_count: u64,
62    pub content: String,
63}
64
65impl EngineContextSourceMaterializationResponseV1 {
66    /// Validate the additive response envelope and the digest of its final
67    /// policy-enforced materialized bytes.
68    pub fn validate(&self) -> Result<(), ValidationError> {
69        if self.schema_version != 1
70            || self.transport_version != 1
71            || self.engine_interface_version.as_str() != "1.0.0"
72            || self.content.len() > MAX_ENGINE_SOURCE_MATERIALIZED_CONTEXT_BYTES
73            || self.materialized_token_count > self.plan.result.plan.budget_tokens
74        {
75            return Err(ValidationError::new(
76                "invalid Engine materialization response envelope",
77            ));
78        }
79        self.plan.validate_binding()?;
80        let actual = sha256_digest(self.content.as_bytes())?;
81        if actual != self.materialized_digest {
82            return Err(ValidationError::new(
83                "Engine materialized content digest mismatch",
84            ));
85        }
86        Ok(())
87    }
88}
89
90fn sha256_digest(bytes: &[u8]) -> Result<Sha256Digest, ValidationError> {
91    let mut digest = String::from("sha256:");
92    for byte in Sha256::digest(bytes).iter() {
93        write!(&mut digest, "{byte:02x}")
94            .map_err(|_| ValidationError::new("Engine materialized digest encoding failed"))?;
95    }
96    Sha256Digest::new(digest)
97}