1use serde::{Deserialize, Serialize};
4
5use crate::{
6 parse_production_runtime_artifact_v1, ProductionArtifactIntegrityViolation,
7 ProductionRuntimeArtifactV1, ResumeBuildId,
8};
9
10#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
11pub enum ProductionValidationPhase {
12 V0,
13 V1,
14 V2,
15 V3,
16 V4,
17 V5,
18 V6,
19 V7,
20 V8,
21 V9,
22 V10,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26#[allow(clippy::struct_excessive_bools)]
27pub struct ProductionValidationEvidence {
28 pub endpoints_valid: bool,
29 pub fingerprints_and_aliases_valid: bool,
30 pub chunk_exports_valid: bool,
31 pub boot_entry_closed: bool,
32 pub resume_products_agree: bool,
33 pub anchor_event_tables_agree: bool,
34 pub cleanup_closure_valid: bool,
35}
36
37impl ProductionValidationEvidence {
38 #[must_use]
39 pub const fn all_valid() -> Self {
40 Self {
41 endpoints_valid: true,
42 fingerprints_and_aliases_valid: true,
43 chunk_exports_valid: true,
44 boot_entry_closed: true,
45 resume_products_agree: true,
46 anchor_event_tables_agree: true,
47 cleanup_closure_valid: true,
48 }
49 }
50}
51
52#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct ProductionRuntimeFailure {
55 pub class: String,
56 pub code: String,
57 pub build_id: ResumeBuildId,
58 pub subject_kind: String,
59 pub subject_id_or_ordinal: String,
60 pub subject_trusted: bool,
61 pub phase: ProductionValidationPhase,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct ProductionValidationResult {
66 pub artifact: ProductionRuntimeArtifactV1,
67 pub completed_phases: Vec<ProductionValidationPhase>,
68}
69
70pub fn validate_production_runtime_pipeline(
76 json: &str,
77 expected_build_id: &ResumeBuildId,
78 evidence: &ProductionValidationEvidence,
79) -> Result<ProductionValidationResult, Box<ProductionRuntimeFailure>> {
80 if contains_forbidden_generic_key(json) {
81 return Err(Box::new(failure(
82 expected_build_id,
83 ProductionValidationPhase::V0,
84 "PSPRD1400",
85 "schema",
86 "untrusted-field",
87 false,
88 )));
89 }
90 let artifact =
91 parse_production_runtime_artifact_v1(json, expected_build_id).map_err(|violations| {
92 let phase = integrity_phase(&violations);
93 Box::new(failure(
94 expected_build_id,
95 phase,
96 "PSPRD1401",
97 "artifact",
98 "untrusted",
99 false,
100 ))
101 })?;
102 let checks = [
103 (ProductionValidationPhase::V4, evidence.endpoints_valid),
104 (
105 ProductionValidationPhase::V5,
106 evidence.fingerprints_and_aliases_valid,
107 ),
108 (ProductionValidationPhase::V6, evidence.chunk_exports_valid),
109 (ProductionValidationPhase::V7, evidence.boot_entry_closed),
110 (
111 ProductionValidationPhase::V8,
112 evidence.resume_products_agree,
113 ),
114 (
115 ProductionValidationPhase::V9,
116 evidence.anchor_event_tables_agree,
117 ),
118 (
119 ProductionValidationPhase::V10,
120 evidence.cleanup_closure_valid,
121 ),
122 ];
123 for (phase, valid) in checks {
124 if !valid {
125 return Err(Box::new(failure(
126 &artifact.build_id,
127 phase,
128 "PSPRD1402",
129 "validation",
130 "closed-product",
131 true,
132 )));
133 }
134 }
135 Ok(ProductionValidationResult {
136 artifact,
137 completed_phases: vec![
138 ProductionValidationPhase::V0,
139 ProductionValidationPhase::V1,
140 ProductionValidationPhase::V2,
141 ProductionValidationPhase::V3,
142 ProductionValidationPhase::V4,
143 ProductionValidationPhase::V5,
144 ProductionValidationPhase::V6,
145 ProductionValidationPhase::V7,
146 ProductionValidationPhase::V8,
147 ProductionValidationPhase::V9,
148 ProductionValidationPhase::V10,
149 ],
150 })
151}
152
153fn integrity_phase(
154 violations: &[ProductionArtifactIntegrityViolation],
155) -> ProductionValidationPhase {
156 if violations.iter().any(|violation| {
157 matches!(
158 violation,
159 ProductionArtifactIntegrityViolation::SchemaVersionMismatch
160 )
161 }) {
162 ProductionValidationPhase::V0
163 } else if violations.iter().any(|violation| {
164 matches!(
165 violation,
166 ProductionArtifactIntegrityViolation::BuildIdMismatch
167 | ProductionArtifactIntegrityViolation::RuntimeProtocolMismatch
168 )
169 }) {
170 ProductionValidationPhase::V1
171 } else if violations.iter().any(|violation| {
172 matches!(
173 violation,
174 ProductionArtifactIntegrityViolation::TableChecksumMismatch(_)
175 | ProductionArtifactIntegrityViolation::ArtifactChecksumMismatch
176 )
177 }) {
178 ProductionValidationPhase::V2
179 } else {
180 ProductionValidationPhase::V3
181 }
182}
183
184fn contains_forbidden_generic_key(json: &str) -> bool {
185 ["\"__proto__\"", "\"constructor\"", "\"prototype\""]
186 .iter()
187 .any(|key| json.contains(key))
188}
189
190fn failure(
191 build_id: &ResumeBuildId,
192 phase: ProductionValidationPhase,
193 code: &str,
194 subject_kind: &str,
195 subject: &str,
196 trusted: bool,
197) -> ProductionRuntimeFailure {
198 ProductionRuntimeFailure {
199 class: "ProductionArtifactMismatch".to_string(),
200 code: code.to_string(),
201 build_id: build_id.clone(),
202 subject_kind: subject_kind.to_string(),
203 subject_id_or_ordinal: subject.to_string(),
204 subject_trusted: trusted,
205 phase,
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::{
213 build_production_runtime_artifact, extract_production_chunk_graph,
214 production_runtime_artifact_json, ExecutableProgramFingerprint, ProductionRootChunkInput,
215 ResumeBoundaryId, ResumeManifest, SharedChunkCandidatePlan,
216 };
217 use std::str::FromStr;
218
219 fn valid_json() -> (String, ResumeBuildId) {
220 let manifest = ResumeManifest {
221 schema_version: 6,
222 build_id: ResumeBuildId::zero_sentinel(),
223 snapshot_schema_version: 1,
224 runtime_protocol_version: 1,
225 application_root_boundary_id: ResumeBoundaryId::from_str("resume-boundary:root")
226 .expect("boundary"),
227 boundaries: Vec::new(),
228 slot_schemas: Vec::new(),
229 capture_programs: Vec::new(),
230 restore_programs: Vec::new(),
231 chunks: Vec::new(),
232 activations: Vec::new(),
233 anchors: Vec::new(),
234 events: Vec::new(),
235 phase_i_component_resume_records: Vec::new(),
236 phase_i_form_resume_records: Vec::new(),
237 };
238 let graph = extract_production_chunk_graph(
239 &SharedChunkCandidatePlan {
240 candidates: Vec::new(),
241 rejections: Vec::new(),
242 },
243 &[ProductionRootChunkInput {
244 activation_root_id: "root".to_string(),
245 root_kind: "interaction".to_string(),
246 programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
247 b"a",
248 )],
249 }],
250 )
251 .expect("graph")
252 .0;
253 let artifact = build_production_runtime_artifact(&manifest, &graph).expect("artifact");
254 (
255 production_runtime_artifact_json(&artifact),
256 manifest.build_id,
257 )
258 }
259
260 #[test]
261 fn k14_runs_all_phases_and_stops_before_authored_execution() {
262 let (json, build_id) = valid_json();
263 let result = validate_production_runtime_pipeline(
264 &json,
265 &build_id,
266 &ProductionValidationEvidence::all_valid(),
267 )
268 .expect("validated pipeline");
269 assert_eq!(result.completed_phases.len(), 11);
270 let mut invalid = ProductionValidationEvidence::all_valid();
271 invalid.cleanup_closure_valid = false;
272 let failure = validate_production_runtime_pipeline(&json, &build_id, &invalid)
273 .expect_err("V10 failure");
274 assert_eq!(failure.phase, ProductionValidationPhase::V10);
275 assert!(!serde_json::to_string(&failure)
276 .expect("failure JSON")
277 .contains("src/"));
278 }
279
280 #[test]
281 fn k14_rejects_prototype_pollution_as_untrusted_v0_input() {
282 let build_id = ResumeBuildId::zero_sentinel();
283 let failure = validate_production_runtime_pipeline(
284 r#"{"__proto__":{}}"#,
285 &build_id,
286 &ProductionValidationEvidence::all_valid(),
287 )
288 .expect_err("prototype key");
289 assert_eq!(failure.phase, ProductionValidationPhase::V0);
290 assert!(!failure.subject_trusted);
291 }
292}