Skip to content

Commit bf1badc

Browse files
Pigbibicodex
andcommitted
feat: validate release runtime evidence identity
Co-Authored-By: Codex <noreply@openai.com>
1 parent 7da93b7 commit bf1badc

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

src/release_contract.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import hashlib
4+
import re
45
from pathlib import Path
56
from typing import Any
67

@@ -62,6 +63,15 @@
6263
"live_pool_legacy": "live_pool_legacy.json",
6364
}
6465
EXPECTED_ARTIFACT_CONTRACT_VERSION = "crypto_live_pool_rotation.live_pool.v1"
66+
REQUIRED_RUNTIME_EVIDENCE_IDENTITY_FIELDS = (
67+
"strategy_profile",
68+
"mode",
69+
"source_revision",
70+
"input_timestamp",
71+
"artifact_contract",
72+
"artifact_version",
73+
"artifacts",
74+
)
6575

6676

6777
def _sha256_file(path: Path) -> str:
@@ -162,6 +172,72 @@ def _normalize_source_project(value: Any, field_label: str, errors: list[str]) -
162172
return normalized
163173

164174

175+
def _is_sha256(value: Any) -> bool:
176+
return isinstance(value, str) and bool(re.fullmatch(r"[0-9a-f]{64}", value.strip()))
177+
178+
179+
def _validate_runtime_evidence_identity(
180+
manifest: dict[str, Any],
181+
artifact_manifest: dict[str, Any],
182+
*,
183+
live_pool_mode: str,
184+
live_pool_version: str,
185+
errors: list[str],
186+
) -> None:
187+
label = "release_manifest.json runtime_evidence_identity"
188+
identity = manifest.get("runtime_evidence_identity")
189+
if not isinstance(identity, dict):
190+
errors.append(f"{label} must be an object")
191+
return
192+
193+
_append_missing_fields(identity, REQUIRED_RUNTIME_EVIDENCE_IDENTITY_FIELDS, errors, label)
194+
if str(identity.get("strategy_profile", "")).strip() != str(
195+
artifact_manifest.get("strategy_profile", "")
196+
).strip():
197+
errors.append(f"{label} strategy_profile does not match artifact_manifest.json")
198+
if str(identity.get("mode", "")).strip() != live_pool_mode:
199+
errors.append(f"{label} mode does not match live_pool.json")
200+
if not re.fullmatch(r"[0-9a-f]{40}", str(identity.get("source_revision", "")).strip()):
201+
errors.append(f"{label} source_revision must be a 40-character lowercase git SHA")
202+
203+
input_timestamp = identity.get("input_timestamp")
204+
try:
205+
timestamp = pd.Timestamp(input_timestamp)
206+
except Exception:
207+
timestamp = None
208+
if not isinstance(input_timestamp, str) or timestamp is None or pd.isna(timestamp) or timestamp.tzinfo is None:
209+
errors.append(f"{label} input_timestamp must be a timezone-aware timestamp")
210+
211+
if str(identity.get("artifact_contract", "")).strip() != str(
212+
artifact_manifest.get("contract_version", "")
213+
).strip():
214+
errors.append(f"{label} artifact_contract does not match artifact_manifest.json")
215+
if str(identity.get("artifact_version", "")).strip() != live_pool_version:
216+
errors.append(f"{label} artifact_version does not match live_pool.json version")
217+
218+
identity_artifacts = identity.get("artifacts")
219+
artifact_manifest_artifacts = artifact_manifest.get("artifacts")
220+
if not isinstance(identity_artifacts, dict):
221+
errors.append(f"{label} artifacts must be an object")
222+
return
223+
if not isinstance(artifact_manifest_artifacts, dict):
224+
return
225+
for artifact_name in REQUIRED_ARTIFACT_MANIFEST_ARTIFACTS:
226+
identity_entry = identity_artifacts.get(artifact_name)
227+
artifact_entry = artifact_manifest_artifacts.get(artifact_name)
228+
if not isinstance(identity_entry, dict):
229+
errors.append(f"{label} artifacts.{artifact_name} must be an object")
230+
continue
231+
identity_sha = identity_entry.get("sha256")
232+
if not _is_sha256(identity_sha):
233+
errors.append(f"{label} artifacts.{artifact_name}.sha256 must be a SHA-256 digest")
234+
continue
235+
if not isinstance(artifact_entry, dict) or identity_sha.strip() != str(artifact_entry.get("sha256", "")).strip():
236+
errors.append(
237+
f"{label} artifacts.{artifact_name}.sha256 does not match artifact_manifest.json"
238+
)
239+
240+
165241
def _coerce_selected_flag(series: pd.Series) -> pd.Series:
166242
if pd.api.types.is_bool_dtype(series):
167243
return series.fillna(False)
@@ -221,6 +297,7 @@ def validate_release_outputs(
221297
max_age_days: int | None = None,
222298
require_manifest: bool = False,
223299
require_artifact_manifest: bool = False,
300+
require_runtime_evidence_identity: bool = False,
224301
require_freshness: bool = False,
225302
) -> dict[str, Any]:
226303
output_path = Path(output_dir)
@@ -567,6 +644,15 @@ def validate_release_outputs(
567644
elif expected_sha != _sha256_file(resolved_path):
568645
errors.append(f"artifact_manifest.json artifacts.{artifact_name}.sha256 does not match file content")
569646

647+
if require_runtime_evidence_identity and manifest_present and artifact_manifest_present:
648+
_validate_runtime_evidence_identity(
649+
manifest,
650+
artifact_manifest,
651+
live_pool_mode=live_pool_mode,
652+
live_pool_version=live_pool_version,
653+
errors=errors,
654+
)
655+
570656
age_days: int | None = None
571657
if live_pool_as_of_ts is not None:
572658
if reference_date is None:

tests/test_release_contract.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def build_outputs(
3333
mode: str = "core_major",
3434
source_project: str = "crypto-live-pool-pipelines",
3535
include_manifest: bool = False,
36+
include_runtime_evidence_identity: bool = False,
3637
) -> None:
3738
output_dir = root / "data" / "output"
3839
output_dir.mkdir(parents=True, exist_ok=True)
@@ -124,6 +125,20 @@ def build_outputs(
124125
)
125126

126127
if include_manifest:
128+
runtime_evidence_identity = {}
129+
if include_runtime_evidence_identity:
130+
artifact_manifest = json.loads(
131+
(output_dir / "artifact_manifest.json").read_text(encoding="utf-8")
132+
)
133+
runtime_evidence_identity = {
134+
"strategy_profile": "crypto_live_pool_rotation",
135+
"mode": mode,
136+
"source_revision": "a" * 40,
137+
"input_timestamp": "2026-03-13T00:00:00Z",
138+
"artifact_contract": artifact_manifest["contract_version"],
139+
"artifact_version": version,
140+
"artifacts": artifact_manifest["artifacts"],
141+
}
127142
write_json(
128143
output_dir / "release_manifest.json",
129144
{
@@ -135,6 +150,7 @@ def build_outputs(
135150
"release_prefix": f"crypto-live-pool-pipelines/releases/{version}",
136151
"current_prefix": "crypto-live-pool-pipelines/current",
137152
"artifacts": {},
153+
"runtime_evidence_identity": runtime_evidence_identity,
138154
"firestore": {
139155
"collection": "strategy",
140156
"document": "CRYPTO_LIVE_POOL_ROTATION_LIVE_POOL",
@@ -175,6 +191,56 @@ def test_validate_release_outputs_accepts_consistent_contract(self) -> None:
175191
self.assertEqual(validation["pool_size"], 5)
176192
self.assertEqual(validation["age_days"], 1)
177193

194+
def test_validate_release_outputs_requires_runtime_evidence_identity(self) -> None:
195+
with tempfile.TemporaryDirectory() as tmp_dir:
196+
root = Path(tmp_dir)
197+
self.build_outputs(
198+
root,
199+
include_manifest=True,
200+
include_runtime_evidence_identity=True,
201+
)
202+
203+
validation = validate_release_outputs(
204+
root / "data" / "output",
205+
require_manifest=True,
206+
require_artifact_manifest=True,
207+
require_runtime_evidence_identity=True,
208+
)
209+
210+
self.assertTrue(validation["ok"])
211+
212+
def test_validate_release_outputs_rejects_incomplete_or_mismatched_runtime_evidence_identity(self) -> None:
213+
with tempfile.TemporaryDirectory() as tmp_dir:
214+
root = Path(tmp_dir)
215+
self.build_outputs(
216+
root,
217+
include_manifest=True,
218+
include_runtime_evidence_identity=True,
219+
)
220+
manifest_path = root / "data" / "output" / "release_manifest.json"
221+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
222+
identity = manifest["runtime_evidence_identity"]
223+
identity.pop("source_revision")
224+
identity["artifacts"]["live_pool"]["sha256"] = "b" * 64
225+
write_json(manifest_path, manifest)
226+
227+
validation = validate_release_outputs(
228+
root / "data" / "output",
229+
require_manifest=True,
230+
require_artifact_manifest=True,
231+
require_runtime_evidence_identity=True,
232+
)
233+
234+
self.assertFalse(validation["ok"])
235+
self.assertIn(
236+
"release_manifest.json runtime_evidence_identity missing field: source_revision",
237+
validation["errors"],
238+
)
239+
self.assertIn(
240+
"release_manifest.json runtime_evidence_identity artifacts.live_pool.sha256 does not match artifact_manifest.json",
241+
validation["errors"],
242+
)
243+
178244
def test_validate_release_outputs_rejects_mismatched_artifact_manifest(self) -> None:
179245
with tempfile.TemporaryDirectory() as tmp_dir:
180246
root = Path(tmp_dir)

0 commit comments

Comments
 (0)