lean_ctx_protocol/
engine_context_materialization.rs1use 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
19pub const MAX_ENGINE_SOURCE_MATERIALIZED_CONTEXT_BYTES: usize =
21 super::MAX_ENGINE_SOURCE_PLAN_REQUEST_BYTES;
22
23#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub planning_evaluation_time: Option<UtcTimestamp>,
34}
35
36impl EngineContextSourceMaterializationRequestV1 {
37 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#[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 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}