ferrum_interfaces/vnext/execution/
validation.rs1use super::{
2 invalid_plan, CapabilityCatalog, CompletionRetentionSpec, Deserialize, ExecutionPlan,
3 PlanBuildRequest, PlanNodeResolution, PlanSchemaVersion, PreparedModelFamily, RuntimePolicy,
4 TrustedExecutionWeightPlan, UnvalidatedExecutionPlan, UnvalidatedExecutionPlanWire, VNextError,
5 EXECUTION_PLAN_SCHEMA, MAX_EXECUTION_PLAN_WIRE_BYTES,
6};
7
8#[derive(Deserialize)]
9struct ExecutionPlanSchemaEnvelope {
10 payload: ExecutionPlanSchemaHeader,
11}
12
13#[derive(Deserialize)]
14struct ExecutionPlanSchemaHeader {
15 schema: PlanSchemaVersion,
16}
17
18pub(super) fn validate_execution_plan_wire_size(
19 wire_size: usize,
20 context: &'static str,
21) -> Result<(), VNextError> {
22 if wire_size > MAX_EXECUTION_PLAN_WIRE_BYTES {
23 return Err(VNextError::Serialization {
24 context,
25 message: format!(
26 "execution plan wire size {wire_size} exceeds limit {MAX_EXECUTION_PLAN_WIRE_BYTES}"
27 ),
28 });
29 }
30 Ok(())
31}
32
33impl ExecutionPlan {
34 pub fn to_json(&self) -> Result<Vec<u8>, VNextError> {
35 let bytes = serde_json::to_vec(self).map_err(|error| VNextError::Serialization {
36 context: "serialize execution plan",
37 message: error.to_string(),
38 })?;
39 validate_execution_plan_wire_size(bytes.len(), "serialize execution plan")?;
40 Ok(bytes)
41 }
42
43 pub fn decode_untrusted(bytes: &[u8]) -> Result<UnvalidatedExecutionPlan, VNextError> {
44 const CONTEXT: &str = "decode untrusted execution plan";
45 validate_execution_plan_wire_size(bytes.len(), CONTEXT)?;
46 let header =
47 serde_json::from_slice::<ExecutionPlanSchemaEnvelope>(bytes).map_err(|error| {
48 VNextError::Serialization {
49 context: CONTEXT,
50 message: error.to_string(),
51 }
52 })?;
53 if header.payload.schema != EXECUTION_PLAN_SCHEMA {
54 return Err(VNextError::UnsupportedPlanSchema {
55 expected_major: EXECUTION_PLAN_SCHEMA.major,
56 expected_minor: EXECUTION_PLAN_SCHEMA.minor,
57 actual_major: header.payload.schema.major,
58 actual_minor: header.payload.schema.minor,
59 });
60 }
61 serde_json::from_slice::<UnvalidatedExecutionPlanWire>(bytes)
62 .map(UnvalidatedExecutionPlan::from)
63 .map_err(|error| VNextError::Serialization {
64 context: CONTEXT,
65 message: error.to_string(),
66 })
67 }
68
69 pub fn from_json_validated<P: RuntimePolicy>(
70 bytes: &[u8],
71 family: &PreparedModelFamily,
72 capabilities: &CapabilityCatalog,
73 policy: &P,
74 node_resolutions: Vec<PlanNodeResolution>,
75 ) -> Result<Self, VNextError> {
76 Self::decode_untrusted(bytes)?.revalidate(family, capabilities, policy, node_resolutions)
77 }
78
79 pub fn from_json_validated_with_completion_retention<P: RuntimePolicy>(
80 bytes: &[u8],
81 family: &PreparedModelFamily,
82 capabilities: &CapabilityCatalog,
83 policy: &P,
84 node_resolutions: Vec<PlanNodeResolution>,
85 completion_retention: CompletionRetentionSpec,
86 ) -> Result<Self, VNextError> {
87 Self::decode_untrusted(bytes)?.revalidate_with_completion_retention(
88 family,
89 capabilities,
90 policy,
91 node_resolutions,
92 completion_retention,
93 )
94 }
95
96 pub fn from_json_validated_with_execution_weights<P: RuntimePolicy>(
97 bytes: &[u8],
98 family: &PreparedModelFamily,
99 capabilities: &CapabilityCatalog,
100 policy: &P,
101 node_resolutions: Vec<PlanNodeResolution>,
102 completion_retention: CompletionRetentionSpec,
103 execution_weights: TrustedExecutionWeightPlan,
104 ) -> Result<Self, VNextError> {
105 Self::decode_untrusted(bytes)?.revalidate_with_execution_weights(
106 family,
107 capabilities,
108 policy,
109 node_resolutions,
110 completion_retention,
111 execution_weights,
112 )
113 }
114
115 pub fn validate_against<P: RuntimePolicy>(
116 &self,
117 family: &PreparedModelFamily,
118 capabilities: &CapabilityCatalog,
119 policy: &P,
120 node_resolutions: &[PlanNodeResolution],
121 ) -> Result<(), VNextError> {
122 self.validate_against_with_completion_retention(
123 family,
124 capabilities,
125 policy,
126 node_resolutions,
127 CompletionRetentionSpec::default(),
128 )
129 }
130
131 pub fn validate_against_with_completion_retention<P: RuntimePolicy>(
132 &self,
133 family: &PreparedModelFamily,
134 capabilities: &CapabilityCatalog,
135 policy: &P,
136 node_resolutions: &[PlanNodeResolution],
137 completion_retention: CompletionRetentionSpec,
138 ) -> Result<(), VNextError> {
139 let rebuilt = ExecutionPlan::build(
140 PlanBuildRequest::new(family, capabilities, policy, node_resolutions.to_vec())?
141 .with_execution_weights(self.trusted_execution_weights.clone())?
142 .with_completion_retention(completion_retention)?,
143 )?;
144 if rebuilt.operation_registry_authority != self.operation_registry_authority {
145 return Err(invalid_plan(
146 "execution plan belongs to a different operation runtime registry",
147 ));
148 }
149 if &rebuilt != self {
150 return Err(invalid_plan(
151 "execution plan is not identical to its semantic rebuild",
152 ));
153 }
154 Ok(())
155 }
156}
157
158impl UnvalidatedExecutionPlan {
159 pub fn schema(&self) -> PlanSchemaVersion {
160 self.payload.schema
161 }
162
163 pub fn revalidate<P: RuntimePolicy>(
164 self,
165 family: &PreparedModelFamily,
166 capabilities: &CapabilityCatalog,
167 policy: &P,
168 node_resolutions: Vec<PlanNodeResolution>,
169 ) -> Result<ExecutionPlan, VNextError> {
170 self.revalidate_with_completion_retention(
171 family,
172 capabilities,
173 policy,
174 node_resolutions,
175 CompletionRetentionSpec::default(),
176 )
177 }
178
179 pub fn revalidate_with_completion_retention<P: RuntimePolicy>(
180 self,
181 family: &PreparedModelFamily,
182 capabilities: &CapabilityCatalog,
183 policy: &P,
184 node_resolutions: Vec<PlanNodeResolution>,
185 completion_retention: CompletionRetentionSpec,
186 ) -> Result<ExecutionPlan, VNextError> {
187 let execution_weights = TrustedExecutionWeightPlan::identity(family)?;
188 self.revalidate_with_execution_weights(
189 family,
190 capabilities,
191 policy,
192 node_resolutions,
193 completion_retention,
194 execution_weights,
195 )
196 }
197
198 pub fn revalidate_with_execution_weights<P: RuntimePolicy>(
199 self,
200 family: &PreparedModelFamily,
201 capabilities: &CapabilityCatalog,
202 policy: &P,
203 node_resolutions: Vec<PlanNodeResolution>,
204 completion_retention: CompletionRetentionSpec,
205 execution_weights: TrustedExecutionWeightPlan,
206 ) -> Result<ExecutionPlan, VNextError> {
207 if self.payload.schema != EXECUTION_PLAN_SCHEMA {
208 return Err(VNextError::UnsupportedPlanSchema {
209 expected_major: EXECUTION_PLAN_SCHEMA.major,
210 expected_minor: EXECUTION_PLAN_SCHEMA.minor,
211 actual_major: self.payload.schema.major,
212 actual_minor: self.payload.schema.minor,
213 });
214 }
215 let rebuilt = ExecutionPlan::build(
216 PlanBuildRequest::new(family, capabilities, policy, node_resolutions)?
217 .with_execution_weights(execution_weights)?
218 .with_completion_retention(completion_retention)?,
219 )?;
220 let untrusted_payload =
221 serde_json::to_value(&self.payload).map_err(|error| VNextError::Serialization {
222 context: "serialize unvalidated execution plan payload",
223 message: error.to_string(),
224 })?;
225 let rebuilt_payload =
226 serde_json::to_value(&rebuilt.payload).map_err(|error| VNextError::Serialization {
227 context: "serialize rebuilt execution plan payload",
228 message: error.to_string(),
229 })?;
230 if untrusted_payload != rebuilt_payload {
231 return Err(invalid_plan(
232 "untrusted plan differs from a semantic rebuild against current dependencies",
233 ));
234 }
235 if rebuilt.plan_hash != self.plan_hash {
236 return Err(VNextError::PlanHashMismatch {
237 expected: rebuilt.plan_hash.to_string(),
238 actual: self.plan_hash.to_string(),
239 });
240 }
241 Ok(rebuilt)
242 }
243}