11from __future__ import annotations
22
33import os
4+ from copy import deepcopy
45from dataclasses import dataclass
6+ import hashlib
57from pathlib import Path
8+ import re
69from typing import Any
710
811import pandas as pd
@@ -53,6 +56,7 @@ class ReleaseArtifacts:
5356 live_pool : dict [str , Any ]
5457 live_pool_legacy : dict [str , Any ]
5558 artifact_manifest : dict [str , Any ]
59+ runtime_evidence_identity : dict [str , Any ]
5660
5761
5862def parse_bool (value : Any , default : bool = False ) -> bool :
@@ -117,6 +121,59 @@ def _require_file(path: Path) -> None:
117121 raise FileNotFoundError (f"Required release artifact is missing: { path } " )
118122
119123
124+ def _sha256_file (path : Path ) -> str :
125+ digest = hashlib .sha256 ()
126+ with path .open ("rb" ) as handle :
127+ for chunk in iter (lambda : handle .read (1024 * 1024 ), b"" ):
128+ digest .update (chunk )
129+ return digest .hexdigest ()
130+
131+
132+ def _validate_runtime_evidence_identity (
133+ * ,
134+ identity : Any ,
135+ artifact_manifest : dict [str , Any ],
136+ live_pool : dict [str , Any ],
137+ paths : dict [str , Path ],
138+ ) -> dict [str , Any ]:
139+ if not isinstance (identity , dict ):
140+ raise ValueError ("artifact_manifest.json runtime_evidence_identity must be an object." )
141+ expected_artifacts = {
142+ "live_pool" : paths ["live_pool.json" ],
143+ "live_pool_legacy" : paths ["live_pool_legacy.json" ],
144+ "latest_ranking" : paths ["latest_ranking.csv" ],
145+ "latest_universe" : paths ["latest_universe.json" ],
146+ }
147+ manifest_artifacts = artifact_manifest .get ("artifacts" )
148+ identity_artifacts = identity .get ("artifacts" )
149+ if not isinstance (manifest_artifacts , dict ) or set (manifest_artifacts ) != set (expected_artifacts ):
150+ raise ValueError ("artifact_manifest.json must bind exactly the four release artifacts." )
151+ if not isinstance (identity_artifacts , dict ) or identity_artifacts != manifest_artifacts :
152+ raise ValueError ("runtime_evidence_identity artifacts must equal artifact_manifest.json artifacts." )
153+ for name , path in expected_artifacts .items ():
154+ entry = manifest_artifacts .get (name )
155+ if (
156+ not isinstance (entry , dict )
157+ or entry .get ("path" ) != path .name
158+ or entry .get ("sha256" ) != _sha256_file (path )
159+ ):
160+ raise ValueError (f"Runtime identity digest mismatch for { name } ." )
161+ if identity .get ("strategy_profile" ) != artifact_manifest .get ("strategy_profile" ):
162+ raise ValueError ("Runtime identity strategy_profile mismatch." )
163+ if identity .get ("mode" ) != live_pool .get ("mode" ):
164+ raise ValueError ("Runtime identity mode mismatch." )
165+ if identity .get ("artifact_contract" ) != artifact_manifest .get ("contract_version" ):
166+ raise ValueError ("Runtime identity artifact_contract mismatch." )
167+ if identity .get ("artifact_version" ) != live_pool .get ("version" ):
168+ raise ValueError ("Runtime identity artifact_version mismatch." )
169+ if not re .fullmatch (r"[0-9a-f]{40}" , str (identity .get ("source_revision" , "" ))):
170+ raise ValueError ("Runtime identity source_revision is invalid." )
171+ expected_timestamp = f"{ live_pool .get ('as_of_date' )} T00:00:00Z"
172+ if identity .get ("input_timestamp" ) != expected_timestamp :
173+ raise ValueError ("Runtime identity input_timestamp mismatch." )
174+ return deepcopy (identity )
175+
176+
120177def load_release_artifacts (output_dir : Path | str , mode : str ) -> ReleaseArtifacts :
121178 output_path = Path (output_dir )
122179 paths = {name : output_path / name for name in REQUIRED_OUTPUT_FILES }
@@ -147,6 +204,12 @@ def load_release_artifacts(output_dir: Path | str, mode: str) -> ReleaseArtifact
147204 raise ValueError ("live_pool_legacy.json must contain a non-empty symbols mapping." )
148205
149206 version = build_release_version (as_of_date , mode )
207+ runtime_evidence_identity = _validate_runtime_evidence_identity (
208+ identity = artifact_manifest .get ("runtime_evidence_identity" ),
209+ artifact_manifest = artifact_manifest ,
210+ live_pool = live_pool ,
211+ paths = paths ,
212+ )
150213 return ReleaseArtifacts (
151214 as_of_date = as_of_date ,
152215 version = version ,
@@ -161,6 +224,7 @@ def load_release_artifacts(output_dir: Path | str, mode: str) -> ReleaseArtifact
161224 live_pool = live_pool ,
162225 live_pool_legacy = live_pool_legacy ,
163226 artifact_manifest = artifact_manifest ,
227+ runtime_evidence_identity = runtime_evidence_identity ,
164228 )
165229
166230
@@ -260,6 +324,7 @@ def build_firestore_payload(
260324 "artifact_contract_version" : str (artifacts .artifact_manifest .get ("contract_version" , "" )),
261325 "generated_at" : generated_at ,
262326 "source_project" : settings .source_project ,
327+ "runtime_evidence_identity" : deepcopy (artifacts .runtime_evidence_identity ),
263328 }
264329
265330
@@ -269,6 +334,8 @@ def build_release_manifest(
269334 storage_layout : dict [str , Any ],
270335 firestore_payload : dict [str , Any ],
271336) -> dict [str , Any ]:
337+ if firestore_payload .get ("runtime_evidence_identity" ) != artifacts .runtime_evidence_identity :
338+ raise ValueError ("Firestore runtime_evidence_identity must equal artifact_manifest.json." )
272339 return {
273340 "version" : artifacts .version ,
274341 "mode" : settings .mode ,
@@ -284,6 +351,7 @@ def build_release_manifest(
284351 "live_pool_legacy" : storage_layout ["objects" ]["live_pool_legacy.json" ],
285352 "artifact_manifest" : storage_layout ["objects" ]["artifact_manifest.json" ],
286353 },
354+ "runtime_evidence_identity" : deepcopy (artifacts .runtime_evidence_identity ),
287355 "firestore" : {
288356 "collection" : settings .firestore_collection ,
289357 "document" : settings .firestore_document ,
@@ -392,8 +460,15 @@ def run_release_publish(
392460 max_age_days = max_age_days ,
393461 require_manifest = True ,
394462 require_artifact_manifest = True ,
463+ require_runtime_evidence_identity = True ,
395464 require_freshness = require_freshness ,
396465 )
466+ artifacts = load_release_artifacts (artifacts .output_dir , settings .mode )
467+ if (
468+ manifest .get ("runtime_evidence_identity" ) != artifacts .runtime_evidence_identity
469+ or firestore_payload .get ("runtime_evidence_identity" ) != artifacts .runtime_evidence_identity
470+ ):
471+ raise ValueError ("Runtime evidence identity changed before publish." )
397472
398473 upload_release_artifacts (settings , artifacts , storage_layout )
399474 publish_firestore_summary (settings , firestore_payload )
0 commit comments