From 0bb58755002a33830f097207e31aa2d526ea78fa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:34:09 -0400 Subject: [PATCH 1/8] Add bundle-pinned local data release manifests --- changelog.d/703.added.md | 1 + src/policyengine/provenance/manifest.py | 133 ++++- .../main_485dc2330_certification.py.txt | 167 ++++++ tests/test_local_data_manifest.py | 556 ++++++++++++++++++ 4 files changed, 853 insertions(+), 4 deletions(-) create mode 100644 changelog.d/703.added.md create mode 100644 tests/fixtures/local_manifest/main_485dc2330_certification.py.txt create mode 100644 tests/test_local_data_manifest.py diff --git a/changelog.d/703.added.md b/changelog.d/703.added.md new file mode 100644 index 000000000..9c630be9b --- /dev/null +++ b/changelog.d/703.added.md @@ -0,0 +1 @@ +Support opt-in local data release manifests pinned by the installed bundle TRO, with fail-closed verification and a distinct runtime certification basis for offline Budget-day runners. diff --git a/src/policyengine/provenance/manifest.py b/src/policyengine/provenance/manifest.py index f3b15fb5d..0e9b97d37 100644 --- a/src/policyengine/provenance/manifest.py +++ b/src/policyengine/provenance/manifest.py @@ -1,6 +1,7 @@ import hashlib import json import os +import re from functools import lru_cache from importlib.resources import files from pathlib import Path @@ -8,7 +9,7 @@ from urllib.parse import quote import requests -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, PrivateAttr from policyengine.provenance.bundle_validation import ( shadowed_dataset_overlays, @@ -140,6 +141,7 @@ def uri(self) -> str: class DataReleaseManifest(BaseModel): + _source_is_local: bool = PrivateAttr(default=False) schema_version: int data_package: PackageVersion dataset_role: Optional[str] = None @@ -187,7 +189,12 @@ class DataCertification(BaseModel): #: Bases whose runtime model legitimately differs from the data's build-time #: model, so the build fingerprint is not expected to match. CLAIM_BASED_COMPATIBILITY = frozenset( - {"legacy_compatible_model_package", "qualified_exact_pair"} + { + "legacy_compatible_model_package", + "qualified_exact_pair", + "legacy_compatible_model_package_local_manifest", + "qualified_exact_pair_local_manifest", + } ) @@ -367,8 +374,84 @@ def get_release_manifest(country_id: str) -> CountryReleaseManifest: return manifest -@lru_cache +def bundled_data_release_manifest_sha256(country_id: str) -> str: + """Read the manifest's byte digest from the installed country bundle TRO. + + The artifact and its arrangement must describe the release this bundle + serves. A missing or stale sidecar cannot authorise a local manifest. + """ + country_manifest = get_release_manifest(country_id) + tro_path = files("policyengine").joinpath( + "data", "bundle", f"{country_id}.trace.tro.jsonld" + ) + artifact_id = "composition/1/artifact/data_release_manifest" + try: + tro = json.loads(tro_path.read_bytes()) + artifacts = [ + artifact + for node in tro["@graph"] + for artifact in node.get("trov:hasComposition", {}).get( + "trov:hasArtifact", [] + ) + if artifact.get("@id") == artifact_id + ] + locations = [ + location.get("trov:hasLocation") + for node in tro["@graph"] + for arrangement in node.get("trov:hasArrangement", []) + for location in arrangement.get("trov:hasArtifactLocation", []) + if location.get("trov:hasArtifact", {}).get("@id") == artifact_id + ] + digest = artifacts[0].get("trov:sha256") if len(artifacts) == 1 else None + if ( + not isinstance(digest, str) + or re.fullmatch(r"[0-9a-fA-F]{64}", digest) is None + or locations != [https_release_manifest_uri(country_manifest.data_package)] + ): + raise ValueError("missing, ambiguous, malformed, or stale manifest pin") + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + raise ValueError( + f"Cannot use a local data release manifest for country '{country_id}': " + f"no valid SHA-256 pin in the installed bundle TRO {tro_path}. " + "Expected SHA-256: unavailable; actual SHA-256: unavailable." + ) from exc + return digest.lower() + + def get_data_release_manifest(country_id: str) -> DataReleaseManifest: + """Load an explicitly configured, bundle-pinned file or the cached fetch. + + Local files are read and verified on every call, including after a remote + manifest was cached. Configure the setting before importing country models. + """ + env_name = f"POLICYENGINE_{country_id.upper()}_DATA_MANIFEST_FILE" + local_file = os.environ.get(env_name) + if local_file is None: + return _fetch_data_release_manifest(country_id) + + expected = bundled_data_release_manifest_sha256(country_id) + try: + source_bytes = Path(local_file).expanduser().read_bytes() + except (OSError, ValueError, RuntimeError) as exc: + raise ValueError( + f"Cannot read {env_name}={local_file!r}: {exc}. " + f"Expected SHA-256: {expected}; actual SHA-256: unavailable." + ) from exc + actual = hashlib.sha256(source_bytes).hexdigest() + if actual != expected: + raise ValueError( + f"Local data release manifest {env_name}={local_file!r} " + f"does not match the installed bundle. Expected SHA-256: {expected}; " + f"actual SHA-256: {actual}." + ) + manifest = DataReleaseManifest.model_validate_json(source_bytes) + manifest.source_sha256 = actual + manifest._source_is_local = True + return _apply_release_revision(manifest, get_release_manifest(country_id)) + + +@lru_cache +def _fetch_data_release_manifest(country_id: str) -> DataReleaseManifest: country_manifest = get_release_manifest(country_id) try: @@ -402,6 +485,13 @@ def get_data_release_manifest(country_id: str) -> DataReleaseManifest: if not isinstance(source_bytes, bytes): source_bytes = response.text.encode("utf-8") data_release_manifest.source_sha256 = hashlib.sha256(source_bytes).hexdigest() + return _apply_release_revision(data_release_manifest, country_manifest) + + +def _apply_release_revision( + data_release_manifest: DataReleaseManifest, + country_manifest: CountryReleaseManifest, +) -> DataReleaseManifest: release_revision = country_manifest.data_package.release_manifest_revision if release_revision is not None: for artifact in data_release_manifest.artifacts.values(): @@ -413,6 +503,11 @@ def get_data_release_manifest(country_id: str) -> DataReleaseManifest: return data_release_manifest +# Preserve the existing cache controls for callers and tests of fetched manifests. +get_data_release_manifest.cache_clear = _fetch_data_release_manifest.cache_clear +get_data_release_manifest.cache_info = _fetch_data_release_manifest.cache_info + + def _specifier_matches(version: str, specifier: str) -> bool: """Match a PEP 440 version specifier (``==``, ``>=``, ``<``, ``,``-joined).""" from packaging.specifiers import InvalidSpecifier, SpecifierSet @@ -433,7 +528,10 @@ def _runtime_core_is_certified( tested with one core, so it holds only for that core. """ - if certification.compatibility_basis != "qualified_exact_pair": + if certification.compatibility_basis not in { + "qualified_exact_pair", + "qualified_exact_pair_local_manifest", + }: return True if runtime_core_version is None: from importlib import metadata @@ -492,6 +590,33 @@ def certify_data_release_compatibility( ) raise exc + certification = _certify_available_data_release( + country_id, + country_manifest, + data_release_manifest, + runtime_model_version, + runtime_data_build_fingerprint, + runtime_core_version, + ) + if data_release_manifest._source_is_local: + return certification.model_copy( + update={ + "compatibility_basis": ( + f"{certification.compatibility_basis}_local_manifest" + ) + } + ) + return certification + + +def _certify_available_data_release( + country_id: str, + country_manifest: CountryReleaseManifest, + data_release_manifest: DataReleaseManifest, + runtime_model_version: str, + runtime_data_build_fingerprint: Optional[str], + runtime_core_version: Optional[str], +) -> DataCertification: # A remote build-model, fingerprint or publisher claim cannot broaden an # exact-pair certificate beyond the model, core and recorded data build # that qualification tested. Check them before any acceptance path returns. diff --git a/tests/fixtures/local_manifest/main_485dc2330_certification.py.txt b/tests/fixtures/local_manifest/main_485dc2330_certification.py.txt new file mode 100644 index 000000000..b439117fb --- /dev/null +++ b/tests/fixtures/local_manifest/main_485dc2330_certification.py.txt @@ -0,0 +1,167 @@ +# Unmodified certification function from policyengine.py main 485dc2330. +def certify_data_release_compatibility( + country_id: str, + runtime_model_version: str, + runtime_data_build_fingerprint: Optional[str] = None, + runtime_core_version: Optional[str] = None, +) -> DataCertification: + country_manifest = get_release_manifest(country_id) + try: + data_release_manifest = get_data_release_manifest(country_id) + except DataReleaseManifestUnavailableError as exc: + bundled_certification = country_manifest.certification + if bundled_certification is not None: + if ( + runtime_data_build_fingerprint is not None + and bundled_certification.data_build_fingerprint is not None + and bundled_certification.compatibility_basis + not in CLAIM_BASED_COMPATIBILITY + and runtime_data_build_fingerprint + != bundled_certification.data_build_fingerprint + ): + raise ValueError( + "Runtime data build fingerprint does not match the bundled " + "data certification." + ) + if ( + bundled_certification.certified_for_model_version + == runtime_model_version + and _runtime_core_is_certified( + bundled_certification, runtime_core_version + ) + ): + return bundled_certification + return DataCertification( + compatibility_basis="unverified_data_release_manifest_unavailable", + certified_for_model_version=runtime_model_version, + data_build_id=bundled_certification.data_build_id, + built_with_model_version=( + bundled_certification.built_with_model_version + ), + built_with_model_git_sha=( + bundled_certification.built_with_model_git_sha + ), + data_build_fingerprint=(bundled_certification.data_build_fingerprint), + certified_by=bundled_certification.certified_by, + ) + raise exc + + # A remote build-model, fingerprint or publisher claim cannot broaden an + # exact-pair certificate beyond the model, core and recorded data build + # that qualification tested. Check them before any acceptance path returns. + bundled_certification = country_manifest.certification + # Named as certification names it: the cut tag when the release has one. + remote_build_id = release_build_id(data_release_manifest.build) + if ( + bundled_certification is not None + and bundled_certification.compatibility_basis == "qualified_exact_pair" + ): + if bundled_certification.certified_for_model_version != runtime_model_version: + raise ValueError( + "Data release manifest is not certified for the runtime model " + f"version {runtime_model_version} in country '{country_id}'; " + "the exact pair requires " + f"{bundled_certification.certified_for_model_version}." + ) + if not _runtime_core_is_certified(bundled_certification, runtime_core_version): + raise ValueError( + "Data release manifest is not certified for the runtime core " + f"version {runtime_core_version} in country '{country_id}'; " + "the exact pair requires " + f"{bundled_certification.certified_for_core_version}." + ) + if ( + bundled_certification.data_build_id is not None + and bundled_certification.data_build_id != remote_build_id + ): + raise ValueError( + "Data release manifest is not certified for the remote data " + f"build {remote_build_id} in country '{country_id}'; the exact " + f"pair requires {bundled_certification.data_build_id}." + ) + built_with_model = ( + data_release_manifest.build.built_with_model_package + if data_release_manifest.build is not None + else None + ) + + if ( + built_with_model is not None + and built_with_model.name != country_manifest.model_package.name + ): + raise ValueError( + "Data release manifest was built with a different model package: " + f"expected {country_manifest.model_package.name}, " + f"got {built_with_model.name}." + ) + + if ( + built_with_model is not None + and built_with_model.version == runtime_model_version + ): + return DataCertification( + compatibility_basis="exact_build_model_version", + certified_for_model_version=runtime_model_version, + data_build_id=release_build_id(data_release_manifest.build), + built_with_model_version=built_with_model.version, + built_with_model_git_sha=built_with_model.git_sha, + data_build_fingerprint=built_with_model.data_build_fingerprint, + ) + + if ( + built_with_model is not None + and built_with_model.data_build_fingerprint is not None + and runtime_data_build_fingerprint is not None + and built_with_model.data_build_fingerprint == runtime_data_build_fingerprint + ): + return DataCertification( + compatibility_basis="matching_data_build_fingerprint", + certified_for_model_version=runtime_model_version, + data_build_id=release_build_id(data_release_manifest.build), + built_with_model_version=built_with_model.version, + built_with_model_git_sha=built_with_model.git_sha, + data_build_fingerprint=built_with_model.data_build_fingerprint, + ) + + for compatible_model_package in data_release_manifest.compatible_model_packages: + if compatible_model_package.name != country_manifest.model_package.name: + continue + if _specifier_matches( + version=runtime_model_version, + specifier=compatible_model_package.specifier, + ): + return DataCertification( + compatibility_basis="legacy_compatible_model_package", + certified_for_model_version=runtime_model_version, + data_build_id=release_build_id(data_release_manifest.build), + built_with_model_version=( + built_with_model.version if built_with_model is not None else None + ), + built_with_model_git_sha=( + built_with_model.git_sha if built_with_model is not None else None + ), + data_build_fingerprint=( + built_with_model.data_build_fingerprint + if built_with_model is not None + else None + ), + ) + + # An automated qualification of exactly this model and data build: the + # bundled certificate names the pair, and the ledger evidence for it is + # enforced when the bundle is built (tests/test_certification_evidence.py). + bundled_certification = country_manifest.certification + if ( + bundled_certification is not None + and bundled_certification.compatibility_basis == "qualified_exact_pair" + and bundled_certification.certified_for_model_version == runtime_model_version + and bundled_certification.qualification_pair_id is not None + and bundled_certification.data_build_id == remote_build_id + and _runtime_core_is_certified(bundled_certification, runtime_core_version) + ): + return bundled_certification + + raise ValueError( + "Data release manifest is not certified for the runtime model version " + f"{runtime_model_version} in country '{country_id}'." + ) diff --git a/tests/test_local_data_manifest.py b/tests/test_local_data_manifest.py new file mode 100644 index 000000000..112599dac --- /dev/null +++ b/tests/test_local_data_manifest.py @@ -0,0 +1,556 @@ +"""The local manifest is an opt-in transport for the bundle's exact bytes. + +The unset cases characterize certification on main (485dc2330), including +fetched, bundled-offline, and refused pairs. No credentials or network are used. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest +import requests +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine.provenance import manifest as manifests + +ENVIRONMENT_VARIABLE = "POLICYENGINE_UK_DATA_MANIFEST_FILE" +MODEL = "2.130.1" +OTHER_MODEL = "2.130.2" +CORE = "3.33.0" +BUILD = "test-data-build" +FINGERPRINT = "sha256:test-build" +_DEFAULT = object() + + +def _payload(): + return { + "schema_version": 1, + "data_package": {"name": "policyengine-uk-data", "version": "test-release"}, + "build": { + "build_id": BUILD, + "built_with_core_package": {"name": "policyengine-core", "version": CORE}, + "built_with_model_package": { + "name": "policyengine-uk", + "version": MODEL, + "git_sha": "abc123", + "data_build_fingerprint": FINGERPRINT, + }, + }, + "default_datasets": {"national": "test_dataset"}, + "artifacts": { + "test_dataset": { + "kind": "microdata", + "path": "test_dataset.h5", + "repo_id": "policyengine/test-data", + "revision": "test-release", + "sha256": "d" * 64, + } + }, + } + + +def _manifest_bytes(payload): + # Whitespace and the final newline are deliberately part of the pin. + return (json.dumps(payload, indent=2) + "\n").encode("utf-8") + + +def _qualified_certification(): + return { + "compatibility_basis": "qualified_exact_pair", + "certified_for_model_version": MODEL, + "certified_for_core_version": CORE, + "data_build_id": BUILD, + "built_with_model_version": MODEL, + "built_with_model_git_sha": "abc123", + "data_build_fingerprint": FINGERPRINT, + "qualification_pair_id": "a" * 64, + "certified_by": "test-bundle", + } + + +def _install_bundle(tmp_path, monkeypatch, payload=None, certification=_DEFAULT): + """Install synthetic package resources, exercising the real TRO pin reader.""" + manifests.get_data_release_manifest.cache_clear() + manifests.get_release_manifest.cache_clear() + payload = payload if payload is not None else _payload() + raw_bytes = _manifest_bytes(payload) + local_path = tmp_path / "release_manifest.json" + local_path.write_bytes(raw_bytes) + resource_root = tmp_path / "package" + bundle_dir = resource_root / "data" / "bundle" + bundle_dir.mkdir(parents=True, exist_ok=True) + country = { + "schema_version": 1, + "country_id": "uk", + "policyengine_version": "6.10.1", + "model_package": {"name": "policyengine-uk", "version": MODEL}, + "data_package": { + "name": "policyengine-uk-data", + "version": "test-release", + "repo_id": "policyengine/test-data", + "release_manifest_revision": "f" * 40, + }, + "default_dataset": "test_dataset", + "certification": ( + _qualified_certification() if certification is _DEFAULT else certification + ), + } + (bundle_dir / "manifest.json").write_text( + json.dumps({"data_releases": {"uk": country}}) + ) + tro = { + "@graph": [ + { + "@id": "tro", + "trov:hasArrangement": [ + { + "trov:hasArtifactLocation": [ + { + "trov:hasArtifact": { + "@id": "composition/1/artifact/data_release_manifest" + }, + "trov:hasLocation": ( + "https://huggingface.co/policyengine/test-data/resolve/" + f"{'f' * 40}/release_manifest.json" + ), + } + ] + } + ], + "trov:hasComposition": { + "@id": "composition/1", + "trov:hasArtifact": [ + { + "@id": "composition/1/artifact/data_release_manifest", + "trov:sha256": hashlib.sha256(raw_bytes).hexdigest(), + } + ], + }, + } + ] + } + trace_path = bundle_dir / "uk.trace.tro.jsonld" + trace_path.write_text(json.dumps(tro)) + monkeypatch.setattr(manifests, "files", lambda package: resource_root) + return local_path, trace_path, raw_bytes + + +@pytest.fixture(autouse=True) +def _isolated_manifest_settings(monkeypatch): + monkeypatch.delenv(ENVIRONMENT_VARIABLE, raising=False) + monkeypatch.delenv("POLICYENGINE_US_DATA_MANIFEST_FILE", raising=False) + manifests.get_data_release_manifest.cache_clear() + manifests.get_release_manifest.cache_clear() + yield + manifests.get_data_release_manifest.cache_clear() + manifests.get_release_manifest.cache_clear() + + +def _certify(**kwargs): + implementation = kwargs.pop( + "_implementation", manifests.certify_data_release_compatibility + ) + return implementation( + "uk", + runtime_model_version=kwargs.pop("runtime_model_version", MODEL), + runtime_core_version=kwargs.pop("runtime_core_version", CORE), + **kwargs, + ) + + +def test_matching_local_manifest_certifies_without_a_network_call( + tmp_path, monkeypatch +): + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with patch.object( + manifests.requests, "get", side_effect=requests.ConnectionError("HF is down") + ) as network: + certification = _certify() + loaded = manifests.get_data_release_manifest("uk") + + network.assert_not_called() + assert ( + certification.compatibility_basis == "exact_build_model_version_local_manifest" + ) + assert certification.certified_for_model_version == MODEL + assert certification.data_build_id == BUILD + assert certification.data_build_fingerprint == FINGERPRINT + assert loaded.source_sha256 == hashlib.sha256(raw_bytes).hexdigest() + assert loaded.artifacts["test_dataset"].revision == "f" * 40 + # Source information stays out of public manifest JSON round trips. + expected = manifests.DataReleaseManifest.model_validate_json(raw_bytes) + expected.artifacts["test_dataset"].revision = "f" * 40 + assert loaded.model_dump(mode="json") == expected.model_dump(mode="json") + + +@settings( + max_examples=80, + deadline=None, + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +@given( + position=st.integers(min_value=0, max_value=len(_manifest_bytes(_payload())) - 1) +) +def test_any_one_byte_mutation_is_refused(tmp_path, monkeypatch, position): + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch) + altered = bytearray(raw_bytes) + altered[position] ^= 1 + local_path.write_bytes(altered) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with patch.object(manifests.requests, "get") as network: + with pytest.raises(ValueError) as error: + _certify() + + network.assert_not_called() + assert hashlib.sha256(raw_bytes).hexdigest() in str(error.value) + assert hashlib.sha256(altered).hexdigest() in str(error.value) + + +def test_another_release_is_refused_even_with_an_environment_digest( + tmp_path, monkeypatch +): + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch) + another_release = _payload() + another_release["build"]["build_id"] = "another-release" + altered_bytes = _manifest_bytes(another_release) + local_path.write_bytes(altered_bytes) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + monkeypatch.setenv( + "POLICYENGINE_UK_DATA_MANIFEST_SHA256", + hashlib.sha256(altered_bytes).hexdigest(), + ) + + with pytest.raises(ValueError) as error: + _certify() + + assert hashlib.sha256(raw_bytes).hexdigest() in str(error.value) + assert hashlib.sha256(altered_bytes).hexdigest() in str(error.value) + + +@pytest.mark.parametrize("failure", ["missing", "unreadable", "directory", "empty"]) +def test_bad_local_path_is_a_hard_error(tmp_path, monkeypatch, failure): + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch) + setting = str(local_path) + if failure == "missing": + local_path.unlink() + elif failure == "unreadable": + read_bytes = Path.read_bytes + + def read_unless_local(path): + if path == local_path: + raise PermissionError("test permission denied") + return read_bytes(path) + + monkeypatch.setattr(Path, "read_bytes", read_unless_local) + elif failure == "directory": + setting = str(tmp_path) + else: + setting = "" + monkeypatch.setenv(ENVIRONMENT_VARIABLE, setting) + + with patch.object(manifests.requests, "get") as network: + with pytest.raises(ValueError) as error: + _certify() + + network.assert_not_called() + assert ENVIRONMENT_VARIABLE in str(error.value) + assert hashlib.sha256(raw_bytes).hexdigest() in str(error.value) + assert "actual" in str(error.value).lower() + + +@pytest.mark.parametrize( + "failure", + [ + "absent-trace", + "absent-pin", + "invalid-pin", + "other-artifact", + "absent-location", + "stale-release-location", + "duplicate-pin", + "duplicate-location", + "malformed-graph", + "malformed-composition", + ], +) +def test_local_manifest_requires_the_bundled_manifest_artifact_pin( + tmp_path, monkeypatch, failure +): + local_path, trace_path, _ = _install_bundle(tmp_path, monkeypatch) + if failure == "absent-trace": + trace_path.unlink() + else: + tro = json.loads(trace_path.read_text()) + artifact = tro["@graph"][0]["trov:hasComposition"]["trov:hasArtifact"][0] + if failure == "absent-pin": + del artifact["trov:sha256"] + elif failure == "invalid-pin": + artifact["trov:sha256"] = "not-a-sha256" + elif failure == "other-artifact": + artifact["@id"] = "composition/1/artifact/dataset" + elif failure == "absent-location": + del tro["@graph"][0]["trov:hasArrangement"] + elif failure == "stale-release-location": + location = tro["@graph"][0]["trov:hasArrangement"][0][ + "trov:hasArtifactLocation" + ][0] + location["trov:hasLocation"] = ( + "https://huggingface.co/policyengine/test-data/resolve/" + "old-release/release_manifest.json" + ) + elif failure == "duplicate-pin": + tro["@graph"][0]["trov:hasComposition"]["trov:hasArtifact"].append( + copy.deepcopy(artifact) + ) + elif failure == "duplicate-location": + locations = tro["@graph"][0]["trov:hasArrangement"][0][ + "trov:hasArtifactLocation" + ] + locations.append(copy.deepcopy(locations[0])) + elif failure == "malformed-graph": + tro["@graph"] = None + else: + tro["@graph"][0]["trov:hasComposition"] = [] + trace_path.write_text(json.dumps(tro)) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with patch.object(manifests.requests, "get") as network: + with pytest.raises(ValueError, match="(?i)(bundle|pin|digest|sha256)"): + _certify() + + network.assert_not_called() + + +@pytest.mark.parametrize("difference", ["model", "core", "build", "model-package"]) +def test_a_valid_digest_cannot_widen_exact_pair_certification( + tmp_path, monkeypatch, difference +): + payload = _payload() + runtime = {} + if difference == "model": + runtime["runtime_model_version"] = OTHER_MODEL + elif difference == "core": + runtime["runtime_core_version"] = "99.0.0" + elif difference == "build": + payload["build"]["build_id"] = "another-build" + else: + payload["build"]["built_with_model_package"]["name"] = "another-model" + local_path, _, _ = _install_bundle(tmp_path, monkeypatch, payload) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with pytest.raises(ValueError, match="not certified|different model package"): + _certify(**runtime) + + +@pytest.mark.parametrize( + ("mode", "expected_basis"), + [ + ("fingerprint", "matching_data_build_fingerprint_local_manifest"), + ("publisher-claim", "legacy_compatible_model_package_local_manifest"), + ("qualified", "qualified_exact_pair_local_manifest"), + ], +) +def test_local_transport_retains_existing_certification_bases( + tmp_path, monkeypatch, mode, expected_basis +): + payload = _payload() + payload["build"]["built_with_model_package"]["version"] = OTHER_MODEL + runtime = {} + if mode == "fingerprint": + runtime["runtime_data_build_fingerprint"] = FINGERPRINT + elif mode == "publisher-claim": + payload["compatible_model_packages"] = [ + {"name": "policyengine-uk", "specifier": f"=={MODEL}"} + ] + local_path, _, _ = _install_bundle(tmp_path, monkeypatch, payload) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + assert _certify(**runtime).compatibility_basis == expected_basis + + +def test_local_fingerprint_mismatch_does_not_create_a_new_acceptance_path( + tmp_path, monkeypatch +): + payload = _payload() + payload["build"]["built_with_model_package"]["version"] = OTHER_MODEL + local_path, _, _ = _install_bundle( + tmp_path, monkeypatch, payload, certification=None + ) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with pytest.raises(ValueError, match="not certified"): + _certify(runtime_data_build_fingerprint="sha256:different") + + +def _response(raw_bytes): + response = Mock() + response.status_code = 200 + response.content = raw_bytes + response.text = raw_bytes.decode("utf-8") + return response + + +def test_setting_local_manifest_after_a_cached_fetch_still_checks_its_pin( + tmp_path, monkeypatch +): + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch) + with patch.object(manifests.requests, "get", return_value=_response(raw_bytes)): + assert _certify().compatibility_basis == "exact_build_model_version" + local_path.write_bytes(raw_bytes + b" ") + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with patch.object(manifests.requests, "get") as network: + with pytest.raises(ValueError): + _certify() + + network.assert_not_called() + + +@pytest.mark.parametrize("change", ["mutation", "deletion"]) +def test_a_successful_local_load_does_not_cache_unchecked_bytes( + tmp_path, monkeypatch, change +): + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get") as network: + assert ( + _certify().compatibility_basis == "exact_build_model_version_local_manifest" + ) + if change == "mutation": + local_path.write_bytes(raw_bytes + b" ") + else: + local_path.unlink() + with pytest.raises(ValueError): + _certify() + network.assert_not_called() + + +@pytest.fixture(scope="module") +def main_certification(): + """Keep the exact main implementation as a differential test oracle.""" + snapshot = ( + Path(__file__).parent + / "fixtures" + / "local_manifest" + / "main_485dc2330_certification.py.txt" + ) + namespace = vars(manifests).copy() + exec(compile(snapshot.read_text(), str(snapshot), "exec"), namespace) + return namespace["certify_data_release_compatibility"] + + +@pytest.mark.parametrize( + "case", + [ + "fetched-exact", + "fetched-fingerprint", + "fetched-claim", + "fetched-qualified", + "fetched-model-mismatch", + "fetched-core-mismatch", + "fetched-build-mismatch", + "offline-bundled", + "offline-model-mismatch", + "offline-fingerprint-mismatch", + ], +) +def test_unset_setting_matches_main_certification_cases( + tmp_path, monkeypatch, main_certification, case +): + """Golden differential of existing release/qualified-pair test scenarios. + + Compare complete results or exact exception types/messages with an unchanged + main 485dc2330 implementation, then check the characterized expected result. + Synthetic resources isolate the comparison from future country release pins. + """ + payload = _payload() + bundled = _qualified_certification() + runtime = {} + expected_error = None + expected = manifests.DataCertification( + compatibility_basis="exact_build_model_version", + certified_for_model_version=MODEL, + data_build_id=BUILD, + built_with_model_version=MODEL, + built_with_model_git_sha="abc123", + data_build_fingerprint=FINGERPRINT, + ) + if case in {"fetched-fingerprint", "fetched-claim", "fetched-qualified"}: + payload["build"]["built_with_model_package"]["version"] = OTHER_MODEL + expected.built_with_model_version = OTHER_MODEL + if case == "fetched-fingerprint": + runtime["runtime_data_build_fingerprint"] = FINGERPRINT + expected.compatibility_basis = "matching_data_build_fingerprint" + elif case == "fetched-claim": + payload["compatible_model_packages"] = [ + {"name": "policyengine-uk", "specifier": f">={MODEL},<3.0.0"} + ] + expected.compatibility_basis = "legacy_compatible_model_package" + elif case in {"fetched-qualified", "offline-bundled"}: + expected = manifests.DataCertification.model_validate(bundled) + elif case == "fetched-model-mismatch": + runtime["runtime_model_version"] = OTHER_MODEL + expected_error = "Data release manifest is not certified for the runtime model" + elif case == "fetched-core-mismatch": + runtime["runtime_core_version"] = "99.0.0" + expected_error = "Data release manifest is not certified for the runtime core" + elif case == "fetched-build-mismatch": + payload["build"]["build_id"] = "another-build" + expected_error = ( + "Data release manifest is not certified for the remote data build" + ) + elif case == "offline-model-mismatch": + runtime["runtime_model_version"] = OTHER_MODEL + expected = manifests.DataCertification( + compatibility_basis="unverified_data_release_manifest_unavailable", + certified_for_model_version=OTHER_MODEL, + data_build_id=BUILD, + built_with_model_version=MODEL, + built_with_model_git_sha="abc123", + data_build_fingerprint=FINGERPRINT, + certified_by="test-bundle", + ) + elif case == "offline-fingerprint-mismatch": + bundled["compatibility_basis"] = "matching_data_build_fingerprint" + runtime["runtime_data_build_fingerprint"] = "sha256:different" + expected_error = "Runtime data build fingerprint does not match the bundled" + _, _, raw_bytes = _install_bundle( + tmp_path, monkeypatch, payload, certification=copy.deepcopy(bundled) + ) + assert ENVIRONMENT_VARIABLE not in manifests.os.environ + network_settings = ( + {"side_effect": requests.Timeout("offline")} + if case.startswith("offline-") + else {"return_value": _response(raw_bytes)} + ) + + def outcome(implementation): + try: + return ( + "ok", + _certify(_implementation=implementation, **runtime).model_dump(), + ) + except ValueError as error: + return ("error", type(error), str(error)) + + with patch.object(manifests.requests, "get", **network_settings) as network: + main_outcome = outcome(main_certification) + manifests.get_data_release_manifest.cache_clear() + current_outcome = outcome(manifests.certify_data_release_compatibility) + + assert current_outcome == main_outcome + if expected_error: + assert current_outcome[0] == "error" + assert expected_error in current_outcome[2] + else: + assert current_outcome == ("ok", expected.model_dump()) + assert network.call_count == 2 From 78e09ed63c3805b8739bc9ea229a0d393c213e46 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:38:39 -0400 Subject: [PATCH 2/8] Record local manifest certification in execution receipts and runner docs --- docs/bundles.md | 63 +++++++ docs/receipts.md | 19 ++ .../schemas/execution_receipt.v1.schema.json | 13 ++ src/policyengine/provenance/receipt.py | 41 +++- tests/test_local_manifest_receipt.py | 176 ++++++++++++++++++ 5 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 tests/test_local_manifest_receipt.py diff --git a/docs/bundles.md b/docs/bundles.md index 21bcd37a9..c715ff6de 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -116,6 +116,69 @@ policyengine bundle manifest 4.19.1 `install` targeted. Use `--venv` or `--python` only to inspect a different target explicitly. +## Local release manifests for Budget-day runners + +Set `POLICYENGINE_UK_DATA_MANIFEST_FILE` before starting a generation process to +read the UK release manifest from that file. The same setting is available as +`POLICYENGINE__DATA_MANIFEST_FILE` for other bundled countries. When set, +it always selects the local file, avoiding a Hugging Face request and its timeout. +An unset setting preserves the existing fetch and certification behavior. + +The trust anchor is the `trov:sha256` on +`composition/1/artifact/data_release_manifest` in the installed country's +`data/bundle/{country}.trace.tro.jsonld`. The file's **raw bytes** must match that +pin. A missing pin, unreadable file, altered bytes, or certification mismatch +stops the process. The model, core, build ID and fingerprint checks still apply; +selecting a local file cannot extend the bundle's certification. + +At G0 each morning, while Hugging Face is available, use the exact installed +release that the runner will use and download and verify the manifest before +enabling the setting. Keep the Hugging Face token in `HUGGING_FACE_TOKEN`, +`HF_TOKEN` or `HUGGINGFACE_HUB_TOKEN`: + +```bash +env -u POLICYENGINE_UK_DATA_MANIFEST_FILE POLICYENGINE_SKIP_COUNTRY_IMPORTS=1 python - <<'PY' +import hashlib +from pathlib import Path + +import requests +from policyengine.provenance.manifest import ( + bundled_data_release_manifest_sha256, get_release_manifest, + hugging_face_auth_headers, https_release_manifest_uri, +) + +expected = bundled_data_release_manifest_sha256("uk") +response = requests.get( + https_release_manifest_uri(get_release_manifest("uk").data_package), + headers=hugging_face_auth_headers(), timeout=30, +) +response.raise_for_status() +actual = hashlib.sha256(response.content).hexdigest() +if actual != expected: + raise RuntimeError(f"Manifest SHA-256: expected {expected}; actual {actual}") +target = Path("data/uk-release-manifest.json") +target.parent.mkdir(parents=True, exist_ok=True) +staged = target.with_suffix(".json.tmp") +staged.write_bytes(response.content) +staged.replace(target) +print(f"Verified {target}: {actual}") +PY +export POLICYENGINE_UK_DATA_MANIFEST_FILE="$PWD/data/uk-release-manifest.json" +``` + +Start a fresh runner process with that setting and record its receipt at G0. +Runtime certification and TRACE output use a distinct basis ending in +`_local_manifest`, such as `exact_build_model_version_local_manifest`; packaged +execution receipts record it as `runtime.data_certification_basis`. The +receipt's certificate keeps the bundle's published basis so certificate +verification remains reproducible. See [execution receipts](receipts.md). +Refresh and verify the file whenever the installed certified release changes. + +For Autumn Budget deployment, ship this support in the same `policyengine` +release as María's weekend Microcosm cut certification. Fetch the manifest for +that released bundle at G0; an earlier cut's manifest will fail its new pin. +Use the normal changelog-fragment release workflow for both changes. + ## Bundle-only PRs Run: diff --git a/docs/receipts.md b/docs/receipts.md index a082f449b..48686f6e8 100644 --- a/docs/receipts.md +++ b/docs/receipts.md @@ -47,6 +47,25 @@ receipt_json = receipt.to_canonical_json() For a country `Microsimulation` built with `managed_microsimulation()`, call `build_execution_receipt_for_microsimulation(sim, years=..., reform=..., baseline=...)`. It reads the dataset that was loaded from `sim.policyengine_bundle`. The file must still hash to the SHA-256 verified when it was loaded, and the receipt must name the dataset, URI, revision and package recorded then; otherwise it is refused, because it would describe bytes that did not run. A simulation loaded from an unmanaged file or URI recorded no load-time hash, so it cannot be receipted. The microsimulation does not record its reform, so pass the reform you built it with. +With `POLICYENGINE_UK_DATA_MANIFEST_FILE` enabled, a packaged-bundle receipt +rechecks runtime certification using the local file and records its distinct +`*_local_manifest` basis in `runtime.data_certification_basis`. Local-file +verification errors stop receipt creation. The certificate's +`compatibility_basis` stays the published bundle's assertion, which +`verify_certificate` can reproduce. Like the other runtime context, this source +annotation is excluded from execution and receipt digests. Caller-supplied +bundles do not claim this packaged-bundle runtime annotation. + +The execution-receipt schema accepts the annotation as an optional string; +compatibility bases in the certificate and TRACE TRO are also strings. The UK +Budget dashboard's receipt adapter uses this package's receipt model and +`verify_certificate`, so it needs no basis allowlist change. When adopting this +release, refresh the dashboard's vendored +`tests/fixtures/w4/execution_receipt.v1.schema.json`: its +`tests/test_released_receipt.py` checks exact equality against the installed +schema. Older consumers that forbid extra runtime fields need the updated +schema before reading these receipts. + There are two ways to bind a policyengine-uk `Scenario`: - A scenario built from `parameter_changes` is bound by content with `scenario_reform_identity(scenario)`. - `Scenario.from_reform(d)` wraps `d` in a closure that cannot be bound by its code, so bind `d` itself with `reform_identity(d, "policyengine-uk.reform_dict/v1")`. diff --git a/src/policyengine/data/schemas/execution_receipt.v1.schema.json b/src/policyengine/data/schemas/execution_receipt.v1.schema.json index 12fc69861..d10d1955b 100644 --- a/src/policyengine/data/schemas/execution_receipt.v1.schema.json +++ b/src/policyengine/data/schemas/execution_receipt.v1.schema.json @@ -473,6 +473,19 @@ "default": null, "title": "Ci Run Url" }, + "data_certification_basis": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Runtime certification basis when an opt-in local data manifest was verified against the packaged bundle TRACE TRO. The certificate's compatibility_basis remains the published bundle's assertion.", + "title": "Data Certification Basis" + }, "platform": { "title": "Platform", "type": "string" diff --git a/src/policyengine/provenance/receipt.py b/src/policyengine/provenance/receipt.py index 12bcbcdbd..1c7b43e04 100644 --- a/src/policyengine/provenance/receipt.py +++ b/src/policyengine/provenance/receipt.py @@ -76,6 +76,7 @@ import json import math import operator +import os import platform import re import struct @@ -113,7 +114,14 @@ from urllib.request import url2pathname import psutil -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_validator, + model_serializer, + model_validator, +) from policyengine.provenance.bytecode import code_shape as _code_shape from policyengine.provenance.bytecode import compile_source @@ -488,6 +496,21 @@ class RuntimeContext(_Contract): python_version: str platform: str ci_run_url: Optional[str] = None + data_certification_basis: Optional[str] = Field( + default=None, + description=( + "Runtime certification basis when an opt-in local data manifest was " + "verified against the packaged bundle TRACE TRO. The certificate's " + "compatibility_basis remains the published bundle's assertion." + ), + ) + + @model_serializer(mode="wrap") + def _omit_absent_local_basis(self, handler): + payload = handler(self) + if self.data_certification_basis is None: + payload.pop("data_certification_basis", None) + return payload class ExecutionReceipt(_Contract): @@ -3871,6 +3894,21 @@ def identity(name: str) -> dict[str, Any]: actual_sha256 = sha256_file(dataset_path) data = _data_identity(country_id, record, bundle, dataset_name, actual_sha256) + runtime_basis = None + if source == "packaged" and ( + f"POLICYENGINE_{country_id.upper()}_DATA_MANIFEST_FILE" in os.environ + ): + from policyengine.provenance.manifest import ( + certify_data_release_compatibility, + ) + + runtime_basis = certify_data_release_compatibility( + country_id, + model.version, + runtime_data_build_fingerprint=data.build_fingerprint, + runtime_core_version=core.version, + ).compatibility_basis + fields = _certificate_fields(country_id, record, bundle) certified_artifact = record.get("certified_data_artifact") or {} certificate = None @@ -3920,6 +3958,7 @@ def identity(name: str) -> dict[str, Any]: python_version=platform.python_version(), platform=sys.platform, ci_run_url=ci_run_url, + data_certification_basis=runtime_basis, ), ) diff --git a/tests/test_local_manifest_receipt.py b/tests/test_local_manifest_receipt.py new file mode 100644 index 000000000..f89a9bbf0 --- /dev/null +++ b/tests/test_local_manifest_receipt.py @@ -0,0 +1,176 @@ +"""Local-manifest runtime evidence preserves the bundle certificate's identity.""" + +import hashlib +import json +from pathlib import Path + +import jsonschema +import pytest + +from policyengine.provenance import certification, manifest, receipt + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCHEMA_PATH = ( + REPO_ROOT / "src/policyengine/data/schemas/execution_receipt.v1.schema.json" +) +LOCAL_BASIS = "exact_build_model_version_local_manifest" + + +@pytest.fixture +def packaged_receipt(monkeypatch, tmp_path): + bundle = json.loads( + (REPO_ROOT / "src/policyengine/data/bundle/manifest.json").read_text() + ) + record = bundle["data_releases"]["uk"] + dataset_path = tmp_path / "dataset.h5" + dataset_path.write_bytes(b"synthetic certified dataset") + digest = hashlib.sha256(dataset_path.read_bytes()).hexdigest() + record["datasets"][record["default_dataset"]]["sha256"] = digest + record["certified_data_artifact"]["sha256"] = digest + bundle_path = tmp_path / "manifest.json" + bundle_path.write_text(json.dumps(bundle)) + versions = { + "policyengine": bundle["policyengine_version"], + "policyengine-uk": record["certification"]["certified_for_model_version"], + "policyengine-core": bundle["packages"]["policyengine-core"]["version"], + } + monkeypatch.setattr( + certification, "default_bundle_source_path", lambda: bundle_path + ) + monkeypatch.setattr( + receipt, + "installed_package_identity", + lambda name, **kwargs: receipt.PackageIdentity( + name=name, version=versions[name] + ), + ) + monkeypatch.setattr(receipt, "loaded_versions", lambda: {}) + monkeypatch.setattr( + receipt, "_refuse_package_changed_after_process_start", lambda name: None + ) + monkeypatch.setattr(receipt, "_projected_years", lambda *args: (2025, 2026)) + monkeypatch.delenv("POLICYENGINE_UK_DATA_MANIFEST_FILE", raising=False) + + def build(*, caller_supplied=False): + current_law = receipt.ReformIdentity(kind="current_law") + return receipt.build_execution_receipt( + country_id="uk", + dataset_path=dataset_path, + years=[2026], + reform=current_law, + baseline=current_law, + bundle=bundle if caller_supplied else None, + ) + + return build, bundle + + +def test_packaged_receipt_records_local_basis_without_changing_certificate( + packaged_receipt, monkeypatch +): + build, bundle = packaged_receipt + calls = [] + + def certify(country_id, model_version, **kwargs): + calls.append((country_id, model_version, kwargs)) + return manifest.DataCertification( + compatibility_basis=LOCAL_BASIS, + certified_for_model_version=model_version, + ) + + monkeypatch.setenv("POLICYENGINE_UK_DATA_MANIFEST_FILE", "/verified/manifest.json") + monkeypatch.setattr(manifest, "certify_data_release_compatibility", certify) + result = build() + assert result.runtime.data_certification_basis == LOCAL_BASIS + assert calls == [ + ( + "uk", + result.model.version, + { + "runtime_core_version": result.core.version, + "runtime_data_build_fingerprint": result.data.build_fingerprint, + }, + ) + ] + assert ( + result.certificate.compatibility_basis + == (bundle["data_releases"]["uk"]["certification"]["compatibility_basis"]) + ) + assert receipt.verify_certificate(result, bundle) == [] + payload = json.loads(result.model_dump_json()) + jsonschema.Draft202012Validator(json.loads(SCHEMA_PATH.read_text())).validate( + payload + ) + assert ( + receipt.ExecutionReceipt.model_validate_json(result.model_dump_json()) == result + ) + + +def test_local_manifest_errors_stop_receipt_building(packaged_receipt, monkeypatch): + build, _ = packaged_receipt + monkeypatch.setenv("POLICYENGINE_UK_DATA_MANIFEST_FILE", "/missing/manifest.json") + + def refuse(*args, **kwargs): + raise ValueError("local manifest: expected SHA-256 aaa; actual SHA-256 bbb") + + monkeypatch.setattr(manifest, "certify_data_release_compatibility", refuse) + with pytest.raises(ValueError, match="expected SHA-256 aaa; actual SHA-256 bbb"): + build() + + +@pytest.mark.parametrize("caller_supplied", [False, True]) +def test_unset_setting_keeps_receipt_building_offline( + packaged_receipt, monkeypatch, caller_supplied +): + build, _ = packaged_receipt + + def unexpected(*args, **kwargs): + raise AssertionError( + "unset local manifest must not cause certification or fetches" + ) + + monkeypatch.setattr(manifest, "certify_data_release_compatibility", unexpected) + monkeypatch.setattr(manifest.requests, "get", unexpected) + result = build(caller_supplied=caller_supplied) + assert result.runtime.data_certification_basis is None + assert ( + "data_certification_basis" + not in json.loads(result.model_dump_json())["runtime"] + ) + + +def test_local_runtime_context_does_not_change_certificate_or_result_digests( + packaged_receipt, monkeypatch +): + build, bundle = packaged_receipt + original = build() + monkeypatch.setenv("POLICYENGINE_UK_DATA_MANIFEST_FILE", "/verified/manifest.json") + monkeypatch.setattr( + manifest, + "certify_data_release_compatibility", + lambda *args, **kwargs: manifest.DataCertification( + compatibility_basis=LOCAL_BASIS, + certified_for_model_version=original.model.version, + ), + ) + local = build() + assert local.runtime.data_certification_basis == LOCAL_BASIS + assert local.certificate == original.certificate + assert local.execution_identity_sha256 == original.execution_identity_sha256 + assert local.receipt_sha256 == original.receipt_sha256 + assert receipt.verify_certificate(local, bundle) == [] + + +def test_caller_supplied_bundle_does_not_claim_packaged_manifest_provenance( + packaged_receipt, monkeypatch +): + build, _ = packaged_receipt + monkeypatch.setenv("POLICYENGINE_UK_DATA_MANIFEST_FILE", "/verified/manifest.json") + + def unexpected(*args, **kwargs): + raise AssertionError("a caller's bundle is not the local file's trust anchor") + + monkeypatch.setattr(manifest, "certify_data_release_compatibility", unexpected) + result = build(caller_supplied=True) + assert result.certificate.certificate_source == "caller_supplied" + assert result.runtime.data_certification_basis is None From fd4e099ed3ff768041f3a537e6e38527c07b0bbf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:43:26 -0400 Subject: [PATCH 3/8] Document local manifest validation and hub handoff --- .job/HANDOFF.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .job/HANDOFF.md diff --git a/.job/HANDOFF.md b/.job/HANDOFF.md new file mode 100644 index 000000000..d2a91e2b6 --- /dev/null +++ b/.job/HANDOFF.md @@ -0,0 +1,47 @@ +# Local manifest fallback handoff + +- Draft PR: https://github.com/PolicyEngine/policyengine.py/pull/704 +- Issue: https://github.com/PolicyEngine/policyengine.py/issues/703 +- Branch: `budget-local-data-manifest`, based on `origin/main` `485dc23302ec4abc6a5f7df5c6c03b8525f64c4d`. +- Reviewed implementation head: `78e09ed63c3805b8739bc9ea229a0d393c213e46` (this handoff is committed immediately afterwards). +- Commits: `0bb58755` local manifest/certification/tests; `78e09ed6` receipt/schema/docs/tests. + +## Design + +`POLICYENGINE__DATA_MANIFEST_FILE` always selects the local file when present. This avoids outage latency and gives deterministic behavior. Unset keeps the original cached network fetch and certification results. + +The pin is the raw-byte `trov:sha256` for `composition/1/artifact/data_release_manifest` in the installed country TRACE TRO. Its arrangement must name the installed release's manifest URI. Missing, malformed, duplicate, or stale pins refuse. The current installed UK pin was checked as `49f9fa820db2e6544ff5cdceeb2d868a504843a6a1c1c06b37188621e700066f`. No environment digest is accepted. Local bytes are re-read and hashed on every call, before JSON parsing/revision rewriting, so cached remote/local loads cannot bypass verification. Invalid paths and digest errors fail closed with digest context. + +Local bytes run through the unchanged certification acceptance logic. All four successful runtime bases gain `_local_manifest`; the claim-based set includes the corresponding local variants. Exact-pair model/core/build restrictions and fingerprint acceptance remain the same. TRACE accepts the basis as a string. Packaged receipts recheck local certification and record it in optional `runtime.data_certification_basis`, keeping the published bundle certificate basis and both digest domains unchanged. Unset receipts omit the new annotation. The schema is regenerated and checked. Caller-supplied bundles do not claim packaged provenance. + +## Validation + +- 38 local-manifest tests passed, including 80 Hypothesis byte mutations and 10 direct main-snapshot differential scenarios. +- 146 existing certification tests passed. +- 96 bundle/bootstrap/tooling tests passed. +- Broad provenance suite: 2,337 passed, 6 skipped, including all 6 new receipt tests. +- Wheel/package-pin tests: 96 passed. +- Total across these final focused runs: **2,713 passed, 6 skipped**. Skips cover private data, Python/model-version-specific cases, and disabled top-level country imports in the broad run. +- `make format`, `make lint`, `git diff --check`, and `scripts/generate_receipt_schemas.py --check` passed. +- Every pytest invocation checked load1 <=110 and `os.statvfs` available bytes >=55,000,000,000. Observed load was below 37 and free disk above 132 GB. No pytest workers were used. +- Local fixtures are synthetic; no private manifest or microdata is committed. + +Detailed parent-run logs are local files under `.job/*tests.log`. + +## Coordination and remaining work + +- Leave PR draft; the hub reviews and merges it. +- Ship in the same policyengine.py release as María's weekend Microcosm cut certification. No versions or country/data pins were changed. `changelog.d/703.added.md` follows the release workflow. +- The G0 morning fetch/verify/atomic-refresh instructions are in `docs/bundles.md`. Refresh the local manifest after adopting the newly certified release. +- Dashboard follow-up: refresh `tests/fixtures/w4/execution_receipt.v1.schema.json` in `PolicyEngine/uk-autumn-budget-dashboard-2026` when upgrading; its receipt adapter has no basis allowlist, but its schema equality test requires the new schema. See `docs/receipts.md`. + +## Workspace Git metadata + +The assigned worktree's protected `.git` points into the caller's repository, where metadata writes are sandbox-blocked. All work and independent Git metadata stayed in this workspace. To inspect the actual branch here: + +```bash +GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git status +GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git log -3 --oneline +``` + +The branch is pushed to the canonical repository. A portable `.job/local-data-manifest.bundle` is also left for harvesting; it contains `budget-local-data-manifest` relative to the main base. The original protected `.git` pointer was not changed. No caller files were written. From 91aa875ee3ad202883e316ec2f5743c9acb32fd6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 05:37:30 -0400 Subject: [PATCH 4/8] Reject conflicting TRO pins and bundle artifact identities --- src/policyengine/provenance/certification.py | 22 +- src/policyengine/provenance/manifest.py | 141 ++++++++++++- tests/test_local_data_manifest.py | 199 +++++++++++++++++++ tests/test_local_manifest_tro_pins.py | 174 ++++++++++++++++ tests/test_release_manifests.py | 124 +++++++++--- tests/test_release_tro_generation.py | 2 +- 6 files changed, 614 insertions(+), 48 deletions(-) create mode 100644 tests/test_local_manifest_tro_pins.py diff --git a/src/policyengine/provenance/certification.py b/src/policyengine/provenance/certification.py index dc773e215..381c1e707 100644 --- a/src/policyengine/provenance/certification.py +++ b/src/policyengine/provenance/certification.py @@ -62,6 +62,12 @@ fetch_pypi_wheel_metadata, release_build_id, ) +from policyengine.provenance.manifest import ( + RELEASE_SCOPED_ARTIFACT_KINDS as RELEASE_SCOPED_ARTIFACT_KINDS, +) +from policyengine.provenance.manifest import ( + artifact_path_for_country_manifest as artifact_path_for_country_manifest, +) MANIFEST_URI_PATTERN = re.compile( r"^hf://(?Pdataset|model)/(?P[^@]+)@(?P[^/]+)" @@ -113,9 +119,6 @@ CONSTITUENCY_ASSET_SPEC.weight_matrix_filename, LOCAL_AUTHORITY_ASSET_SPEC.weight_matrix_filename, ) -# Artifact kinds published beside the release manifest rather than at the repo -# root, so their manifest paths are relative to the release directory. -RELEASE_SCOPED_ARTIFACT_KINDS = frozenset({"diagnostics", "evidence"}) US_STATE_CODES = ( "AL", "AK", @@ -526,19 +529,6 @@ def head_release_file( return response.status_code == 200 -def artifact_path_for_country_manifest(artifact, uri_parts: dict) -> str: - path = artifact.path - release_dir = release_manifest_dir(uri_parts) - if ( - artifact.kind in RELEASE_SCOPED_ARTIFACT_KINDS - and artifact.repo_id == uri_parts["repo_id"] - and artifact.revision == uri_parts["revision"] - and not path.startswith(f"{release_dir}/") - ): - return f"{release_dir}/{path}" - return path - - def artifact_reference_url( reference: dict, uri_parts: dict, prefix: str = "datasets/" ) -> str: diff --git a/src/policyengine/provenance/manifest.py b/src/policyengine/provenance/manifest.py index 0e9b97d37..f06c4bc12 100644 --- a/src/policyengine/provenance/manifest.py +++ b/src/policyengine/provenance/manifest.py @@ -5,7 +5,7 @@ from functools import lru_cache from importlib.resources import files from pathlib import Path -from typing import Literal, Optional +from typing import Literal, NoReturn, Optional from urllib.parse import quote import requests @@ -18,6 +18,8 @@ HF_REQUEST_TIMEOUT_SECONDS = 30 PYPI_REQUEST_TIMEOUT_SECONDS = 30 +# Producer paths for these artifacts are relative to the release directory. +RELEASE_SCOPED_ARTIFACT_KINDS = frozenset({"diagnostics", "evidence"}) class DataReleaseManifestUnavailableError(ValueError): @@ -374,11 +376,27 @@ def get_release_manifest(country_id: str) -> CountryReleaseManifest: return manifest +def _tro_nodes_with_id(value: object, node_id: str) -> list[dict]: + """Collect node definitions and references wherever JSON-LD nests them.""" + nodes: list[dict] = [] + if isinstance(value, dict): + if value.get("@id") == node_id: + nodes.append(value) + for child in value.values(): + nodes.extend(_tro_nodes_with_id(child, node_id)) + elif isinstance(value, list): + for child in value: + nodes.extend(_tro_nodes_with_id(child, node_id)) + return nodes + + def bundled_data_release_manifest_sha256(country_id: str) -> str: """Read the manifest's byte digest from the installed country bundle TRO. The artifact and its arrangement must describe the release this bundle - serves. A missing or stale sidecar cannot authorise a local manifest. + serves. All assertions for the artifact's JSON-LD identifier must agree, + including definitions outside the inline composition. A missing or stale + sidecar cannot authorise a local manifest. """ country_manifest = get_release_manifest(country_id) tro_path = files("policyengine").joinpath( @@ -409,6 +427,19 @@ def bundled_data_release_manifest_sha256(country_id: str) -> str: or locations != [https_release_manifest_uri(country_manifest.data_package)] ): raise ValueError("missing, ambiguous, malformed, or stale manifest pin") + asserted_digests = set() + for node in _tro_nodes_with_id(tro, artifact_id): + if "trov:sha256" not in node: + continue + assertion = node["trov:sha256"] + if ( + not isinstance(assertion, str) + or re.fullmatch(r"[0-9a-fA-F]{64}", assertion) is None + ): + raise ValueError("malformed manifest pin assertion") + asserted_digests.add(assertion.lower()) + if asserted_digests != {digest.lower()}: + raise ValueError("conflicting manifest pin assertions") except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: raise ValueError( f"Cannot use a local data release manifest for country '{country_id}': " @@ -418,7 +449,7 @@ def bundled_data_release_manifest_sha256(country_id: str) -> str: return digest.lower() -def get_data_release_manifest(country_id: str) -> DataReleaseManifest: +def _load_data_release_manifest(country_id: str) -> DataReleaseManifest: """Load an explicitly configured, bundle-pinned file or the cached fetch. Local files are read and verified on every call, including after a remote @@ -447,7 +478,10 @@ def get_data_release_manifest(country_id: str) -> DataReleaseManifest: manifest = DataReleaseManifest.model_validate_json(source_bytes) manifest.source_sha256 = actual manifest._source_is_local = True - return _apply_release_revision(manifest, get_release_manifest(country_id)) + country_manifest = get_release_manifest(country_id) + _apply_release_revision(manifest, country_manifest) + _validate_bundle_artifacts(manifest, country_manifest) + return manifest @lru_cache @@ -485,7 +519,9 @@ def _fetch_data_release_manifest(country_id: str) -> DataReleaseManifest: if not isinstance(source_bytes, bytes): source_bytes = response.text.encode("utf-8") data_release_manifest.source_sha256 = hashlib.sha256(source_bytes).hexdigest() - return _apply_release_revision(data_release_manifest, country_manifest) + _apply_release_revision(data_release_manifest, country_manifest) + _validate_bundle_artifacts(data_release_manifest, country_manifest) + return data_release_manifest def _apply_release_revision( @@ -503,9 +539,98 @@ def _apply_release_revision( return data_release_manifest -# Preserve the existing cache controls for callers and tests of fetched manifests. -get_data_release_manifest.cache_clear = _fetch_data_release_manifest.cache_clear -get_data_release_manifest.cache_info = _fetch_data_release_manifest.cache_info +def artifact_path_for_country_manifest( + artifact: DataReleaseArtifact, uri_parts: dict +) -> str: + """Resolve producer-relative evidence paths as bundle certification does.""" + path = artifact.path + release_dir = uri_parts["path"].rsplit("/", 1)[0] + if ( + artifact.kind in RELEASE_SCOPED_ARTIFACT_KINDS + and artifact.repo_id == uri_parts["repo_id"] + and artifact.revision == uri_parts["revision"] + and not path.startswith(f"{release_dir}/") + ): + return f"{release_dir}/{path}" + return path + + +def _validate_bundle_artifacts( + manifest: DataReleaseManifest, country: CountryReleaseManifest +) -> None: + """Bind a loaded manifest to the artifacts this installed bundle serves. + + Run after revision rewriting. Regional releases and additive overlays may + supply records absent from the primary manifest; every overlapping record + must agree, and the certified/default artifact must always be present. + """ + + def refuse(dataset: str, detail: str) -> NoReturn: + raise ValueError( + f"Data release manifest artifact {dataset!r} does not match the " + f"installed bundle for country {country.country_id!r}: {detail}." + ) + + default = country.default_dataset + declared_default = manifest.default_datasets.get("national") + if declared_default is not None and declared_default != default: + refuse(default, f"manifest declares default {declared_default!r}") + if default not in manifest.artifacts: + refuse(default, "missing default dataset artifact") + certified = country.certified_data_artifact + if certified is not None: + artifact = manifest.artifacts.get(certified.dataset) + if artifact is None: + refuse(certified.dataset, "missing certified dataset artifact") + if artifact.uri != certified.uri: + refuse(certified.dataset, f"expected {certified.uri}, got {artifact.uri}") + if certified.sha256 is not None and artifact.sha256 != certified.sha256: + refuse(certified.dataset, "certified SHA-256 pin differs") + if default not in country.datasets and ( + certified is None or certified.dataset != default + ): + refuse(default, "missing bundled default artifact record") + + uri_parts = { + "repo_id": country.data_package.repo_id, + "revision": _artifact_revision(country.data_package), + "path": country.data_package.release_manifest_path, + } + for name, artifact in manifest.artifacts.items(): + reference = country.datasets.get(name) + if reference is None: + continue + expected_uri = build_hf_uri( + repo_id=reference.repo_id or country.data_package.repo_id, + path_in_repo=reference.path, + revision=reference.revision or _artifact_revision(country.data_package), + ) + actual_uri = build_hf_uri( + repo_id=artifact.repo_id, + path_in_repo=artifact_path_for_country_manifest(artifact, uri_parts), + revision=artifact.revision, + ) + if actual_uri != expected_uri: + refuse(name, f"expected {expected_uri}, got {actual_uri}") + if (artifact.repo_type or country.data_package.repo_type) != ( + reference.repo_type or country.data_package.repo_type + ): + refuse(name, "repository type differs") + if reference.sha256 is not None and artifact.sha256 != reference.sha256: + refuse(name, "managed SHA-256 pin differs") + + +class _DataReleaseManifestLoader: + """Callable loader retaining the fetched manifest's typed cache controls.""" + + def __call__(self, country_id: str) -> DataReleaseManifest: + return _load_data_release_manifest(country_id) + + cache_clear = staticmethod(_fetch_data_release_manifest.cache_clear) + cache_info = staticmethod(_fetch_data_release_manifest.cache_info) + + +get_data_release_manifest = _DataReleaseManifestLoader() def _specifier_matches(version: str, specifier: str) -> bool: diff --git a/tests/test_local_data_manifest.py b/tests/test_local_data_manifest.py index 112599dac..9b27e2938 100644 --- a/tests/test_local_data_manifest.py +++ b/tests/test_local_data_manifest.py @@ -97,6 +97,18 @@ def _install_bundle(tmp_path, monkeypatch, payload=None, certification=_DEFAULT) "release_manifest_revision": "f" * 40, }, "default_dataset": "test_dataset", + "certified_data_artifact": { + "dataset": "test_dataset", + "uri": f"hf://policyengine/test-data/test_dataset.h5@{'f' * 40}", + "sha256": "d" * 64, + }, + "datasets": { + "test_dataset": { + "path": "test_dataset.h5", + "revision": "f" * 40, + "sha256": "d" * 64, + } + }, "certification": ( _qualified_certification() if certification is _DEFAULT else certification ), @@ -399,6 +411,193 @@ def _response(raw_bytes): return response +def _edit_bundle(trace_path, edit): + bundle_path = trace_path.with_name("manifest.json") + bundle = json.loads(bundle_path.read_text()) + edit(bundle["data_releases"]["uk"]) + bundle_path.write_text(json.dumps(bundle)) + manifests.get_release_manifest.cache_clear() + + +@pytest.mark.parametrize("transport", ["local", "fetched"]) +@pytest.mark.parametrize( + ("field", "value"), + [ + ("repo_id", "adversary/elsewhere"), + ("path", "elsewhere.h5"), + ("revision", "unqualified-revision"), + ("sha256", "0" * 64), + ("sha256", None), + ("repo_type", "dataset"), + ("location", "adversary/elsewhere/elsewhere.h5"), + ], +) +def test_pinned_manifest_cannot_change_the_bundled_default_artifact( + tmp_path, monkeypatch, transport, field, value +): + payload = _payload() + artifact = payload["artifacts"]["test_dataset"] + if field == "location": + artifact.update(repo_id="adversary/elsewhere", path="elsewhere.h5") + else: + artifact[field] = value + # Install a TRO matching these exact conflicting bytes while keeping the + # bundle's original certified URI, build, model and fingerprint unchanged. + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch, payload) + if transport == "local": + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + + with patch.object( + manifests.requests, "get", return_value=_response(raw_bytes) + ) as network: + with pytest.raises(ValueError, match="artifact.*installed bundle"): + _certify() + assert network.call_count == (transport == "fetched") + + +@pytest.mark.parametrize("transport", ["local", "fetched"]) +@pytest.mark.parametrize("failure", ["missing-artifact", "other-default"]) +def test_manifest_requires_the_bundled_default_dataset( + tmp_path, monkeypatch, transport, failure +): + payload = _payload() + if failure == "missing-artifact": + payload["artifacts"].clear() + else: + payload["default_datasets"]["national"] = "another_dataset" + local_path, _, raw_bytes = _install_bundle(tmp_path, monkeypatch, payload) + if transport == "local": + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get", return_value=_response(raw_bytes)): + with pytest.raises(ValueError, match="artifact.*installed bundle"): + _certify() + + +@pytest.mark.parametrize("transport", ["local", "fetched"]) +@pytest.mark.parametrize("certified_record", [True, False]) +def test_consistent_default_uses_rewritten_revision( + tmp_path, monkeypatch, transport, certified_record +): + local_path, trace_path, raw_bytes = _install_bundle(tmp_path, monkeypatch) + if not certified_record: + _edit_bundle(trace_path, lambda country: country.pop("certified_data_artifact")) + if transport == "local": + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get", return_value=_response(raw_bytes)): + assert _certify().compatibility_basis == ( + "exact_build_model_version" + + ("_local_manifest" if transport == "local" else "") + ) + assert ( + manifests.get_data_release_manifest("uk").artifacts["test_dataset"].revision + == "f" * 40 + ) + + +@pytest.mark.parametrize("transport", ["local", "fetched"]) +def test_a_different_certified_artifact_cannot_leave_the_default_unpinned( + tmp_path, monkeypatch, transport +): + payload = _payload() + payload["artifacts"]["certified_other"] = dict(payload["artifacts"]["test_dataset"]) + local_path, trace_path, raw_bytes = _install_bundle(tmp_path, monkeypatch, payload) + + def edit(country): + country["datasets"].clear() + country["certified_data_artifact"]["dataset"] = "certified_other" + + _edit_bundle(trace_path, edit) + if transport == "local": + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get", return_value=_response(raw_bytes)): + with pytest.raises(ValueError, match="missing bundled default artifact record"): + _certify() + + +@pytest.mark.parametrize("transport", ["local", "fetched"]) +@pytest.mark.parametrize( + "difference", ["none", "repo_id", "path", "revision", "sha256", "repo_type"] +) +def test_every_overlapping_managed_artifact_must_match( + tmp_path, monkeypatch, transport, difference +): + payload = _payload() + payload["artifacts"]["supplement"] = { + "kind": "microdata", + "path": "inherited.h5", + "repo_id": "policyengine/inherited-data", + "revision": "inherited-release", + "repo_type": "dataset", + "sha256": "b" * 64, + } + reference = dict(payload["artifacts"]["supplement"]) + reference.pop("kind") + if difference != "none": + payload["artifacts"]["supplement"][difference] = ( + "model" if difference == "repo_type" else "conflicting-pin" + ) + local_path, trace_path, raw_bytes = _install_bundle(tmp_path, monkeypatch, payload) + _edit_bundle( + trace_path, + lambda country: country["datasets"].update( + supplement=reference, + regional_only={"path": "regional.h5", "revision": "regional-release"}, + ), + ) + if transport == "local": + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get", return_value=_response(raw_bytes)): + if difference == "none": + _certify() + else: + with pytest.raises( + ValueError, match="artifact 'supplement'.*installed bundle" + ): + _certify() + + +@pytest.mark.parametrize("kind", ["diagnostics", "evidence"]) +@pytest.mark.parametrize("prefixed", [False, True]) +def test_release_relative_paths_match_certification_normalization( + tmp_path, monkeypatch, kind, prefixed +): + payload = _payload() + relative_path = "report.json" + bundled_path = "releases/test-release/report.json" + payload["artifacts"]["report"] = { + "kind": kind, + "path": bundled_path if prefixed else relative_path, + "repo_id": "policyengine/test-data", + "revision": "test-release", + "sha256": "b" * 64, + } + local_path, trace_path, _ = _install_bundle(tmp_path, monkeypatch, payload) + + def edit(country): + country["data_package"]["release_manifest_path"] = ( + "releases/test-release/release_manifest.json" + ) + country["datasets"]["report"] = { + "path": bundled_path, + "revision": "f" * 40, + "sha256": "b" * 64, + } + + _edit_bundle(trace_path, edit) + tro = json.loads(trace_path.read_text()) + tro["@graph"][0]["trov:hasArrangement"][0]["trov:hasArtifactLocation"][0][ + "trov:hasLocation" + ] = ( + "https://huggingface.co/policyengine/test-data/resolve/" + f"{'f' * 40}/releases/test-release/release_manifest.json" + ) + trace_path.write_text(json.dumps(tro)) + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get") as network: + _certify() + network.assert_not_called() + + def test_setting_local_manifest_after_a_cached_fetch_still_checks_its_pin( tmp_path, monkeypatch ): diff --git a/tests/test_local_manifest_tro_pins.py b/tests/test_local_manifest_tro_pins.py new file mode 100644 index 000000000..fed5a9786 --- /dev/null +++ b/tests/test_local_manifest_tro_pins.py @@ -0,0 +1,174 @@ +"""A TRO's manifest pin includes every assertion for the JSON-LD node ID.""" + +from __future__ import annotations + +import json + +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine.provenance import manifest as manifests + +ARTIFACT_ID = "composition/1/artifact/data_release_manifest" +PIN = "a" * 64 + + +def _tro(): + return { + "@graph": [ + { + "@id": "tro", + "trov:hasComposition": { + "trov:hasArtifact": [{"@id": ARTIFACT_ID, "trov:sha256": PIN}] + }, + "trov:hasArrangement": [ + { + "trov:hasArtifactLocation": [ + { + "trov:hasArtifact": {"@id": ARTIFACT_ID}, + "trov:hasLocation": ( + "https://huggingface.co/policyengine/test-data/" + "resolve/test-release/release_manifest.json" + ), + } + ] + } + ], + } + ] + } + + +@pytest.fixture +def read_pin(tmp_path, monkeypatch): + bundle_dir = tmp_path / "data" / "bundle" + bundle_dir.mkdir(parents=True) + country_manifest = manifests.CountryReleaseManifest( + country_id="uk", + policyengine_version="6.10.1", + model_package=manifests.PackageVersion(name="policyengine-uk", version="test"), + data_package=manifests.DataPackageVersion( + name="policyengine-uk-data", + version="test-release", + repo_id="policyengine/test-data", + ), + default_dataset="test_dataset", + ) + monkeypatch.setattr(manifests, "files", lambda package: tmp_path) + monkeypatch.setattr( + manifests, "get_release_manifest", lambda country: country_manifest + ) + + def read(tro): + (bundle_dir / "uk.trace.tro.jsonld").write_text(json.dumps(tro)) + return manifests.bundled_data_release_manifest_sha256("uk") + + return read + + +@pytest.mark.parametrize("before_inline_definition", [False, True]) +def test_direct_graph_conflicting_manifest_pin_is_refused( + read_pin, before_inline_definition +): + tro = _tro() + duplicate = {"@id": ARTIFACT_ID, "trov:sha256": "0" * 64} + if before_inline_definition: + tro["@graph"].insert(0, duplicate) + else: + tro["@graph"].append(duplicate) + + with pytest.raises(ValueError, match="no valid SHA-256 pin"): + read_pin(tro) + + +@pytest.mark.parametrize( + "assertion", + [ + [PIN], + [PIN, "0" * 64], + {"@value": PIN}, + {"@value": "0" * 64}, + "not-a-sha256", + None, + 42, + ], +) +def test_noncanonical_duplicate_hash_assertions_fail_closed(read_pin, assertion): + tro = _tro() + tro["@graph"].append({"@id": ARTIFACT_ID, "trov:sha256": assertion}) + + with pytest.raises(ValueError, match="no valid SHA-256 pin"): + read_pin(tro) + + +def test_identical_external_definitions_and_references_do_not_add_a_hash(read_pin): + tro = _tro() + tro["@graph"].extend( + [ + {"@id": ARTIFACT_ID, "trov:sha256": PIN.upper()}, + {"extension": {"@id": ARTIFACT_ID, "trov:sha256": PIN}}, + {"@id": ARTIFACT_ID}, + ] + ) + + assert read_pin(tro) == PIN + + +def test_hash_on_an_existing_artifact_reference_is_checked(read_pin): + tro = _tro() + reference = tro["@graph"][0]["trov:hasArrangement"][0]["trov:hasArtifactLocation"][ + 0 + ]["trov:hasArtifact"] + reference["trov:sha256"] = "0" * 64 + + with pytest.raises(ValueError, match="no valid SHA-256 pin"): + read_pin(tro) + + +@settings( + max_examples=80, + deadline=None, + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +@given( + placement=st.sampled_from( + ["graph", "document", "tro", "composition", "arrangement", "location"] + ), + wrappers=st.lists( + st.sampled_from(["list", "property", "graph"]), min_size=0, max_size=8 + ), + conflicting_hash=st.binary(min_size=32, max_size=32) + .map(bytes.hex) + .filter(lambda digest: digest != PIN), +) +def test_conflicting_pin_is_refused_at_any_duplicate_placement( + read_pin, placement, wrappers, conflicting_hash +): + tro = _tro() + duplicate = {"@id": ARTIFACT_ID, "trov:sha256": conflicting_hash} + for wrapper in wrappers: + if wrapper == "list": + duplicate = [duplicate] + elif wrapper == "graph": + duplicate = {"@graph": [duplicate]} + else: + duplicate = {"extension": duplicate} + + root = tro["@graph"][0] + if placement == "graph": + # Keep the surrounding graph canonical so refusal must inspect the + # nested assertion, rather than fail on an unrelated malformed node. + tro["@graph"].append({"extension": duplicate}) + else: + target = { + "document": tro, + "tro": root, + "composition": root["trov:hasComposition"], + "arrangement": root["trov:hasArrangement"][0], + "location": root["trov:hasArrangement"][0]["trov:hasArtifactLocation"][0], + }[placement] + target["extension"] = duplicate + + with pytest.raises(ValueError, match="no valid SHA-256 pin"): + read_pin(tro) diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index aff21577a..006070e2a 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -154,6 +154,52 @@ def _response_with_json(payload: dict) -> MagicMock: return response +def _legacy_us_country_manifest( + version: str = "1.73.0", package_name: str = "policyengine-us-data" +) -> CountryReleaseManifest: + """Pin the legacy fixture independently of the installed Populace bundle.""" + return CountryReleaseManifest.model_validate( + { + "country_id": "us", + "policyengine_version": POLICYENGINE_VERSION, + "model_package": {"name": "policyengine-us", "version": "1.653.3"}, + "data_package": { + "name": package_name, + "version": version, + "repo_id": "policyengine/policyengine-us-data", + }, + "default_dataset": "enhanced_cps_2024", + "datasets": { + "enhanced_cps_2024": { + "path": "enhanced_cps_2024.h5", + "revision": version, + "sha256": "abc", + } + }, + "certified_data_artifact": { + "dataset": "enhanced_cps_2024", + "uri": ( + "hf://policyengine/policyengine-us-data/" + f"enhanced_cps_2024.h5@{version}" + ), + "sha256": "abc", + }, + } + ) + + +def _legacy_us_artifacts(version: str = "1.73.0") -> dict: + return { + "enhanced_cps_2024": { + "kind": "microdata", + "path": "enhanced_cps_2024.h5", + "repo_id": "policyengine/policyengine-us-data", + "revision": version, + "sha256": "abc", + } + } + + def _country_modules_with_microsimulation( name: str, microsimulation: MagicMock, @@ -535,10 +581,16 @@ def test__given_country__then_can_fetch_data_release_manifest(self): }, } - with patch( - "policyengine.provenance.manifest.requests.get", - return_value=_response_with_json(payload), - ) as mock_get: + with ( + patch( + "policyengine.provenance.manifest.get_release_manifest", + return_value=_legacy_us_country_manifest("1.78.2"), + ), + patch( + "policyengine.provenance.manifest.requests.get", + return_value=_response_with_json(payload), + ) as mock_get, + ): manifest = get_data_release_manifest("us") assert manifest.schema_version == 1 @@ -561,8 +613,8 @@ def test__given_country__then_can_fetch_data_release_manifest(self): ) mock_get.assert_called_once() assert mock_get.call_args.args[0] == ( - "https://huggingface.co/datasets/policyengine/populace-us/resolve/" - f"{US_DATA_RELEASE_REVISION}/{US_DATA_RELEASE_PATH}" + "https://huggingface.co/policyengine/policyengine-us-data/resolve/" + "1.78.2/release_manifest.json" ) def test__given_explicit_manifest_revision__then_builds_manifest_url(self): @@ -589,7 +641,9 @@ def test__given_release_manifest_artifact_uses_version_tag__then_rewrites_to_com "path": "populace_us_2024.h5", "repo_id": "policyengine/populace-us", "revision": US_DATA_RELEASE_VERSION, - "sha256": "abc", + "sha256": ( + "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" + ), "size_bytes": 123, } }, @@ -651,12 +705,18 @@ def test__given_range_specifier__then_certification_accepts_compatible_version( } ], "default_datasets": {"national": "enhanced_cps_2024"}, - "artifacts": {}, + "artifacts": _legacy_us_artifacts(), } - with patch( - "policyengine.provenance.manifest.requests.get", - return_value=_response_with_json(payload), + with ( + patch( + "policyengine.provenance.manifest.get_release_manifest", + return_value=_legacy_us_country_manifest(), + ), + patch( + "policyengine.provenance.manifest.requests.get", + return_value=_response_with_json(payload), + ), ): certification = certify_data_release_compatibility( "us", @@ -685,12 +745,18 @@ def test__given_matching_fingerprint__then_certification_allows_reuse(self): }, "compatible_model_packages": [], "default_datasets": {"national": "enhanced_cps_2024"}, - "artifacts": {}, + "artifacts": _legacy_us_artifacts(), } - with patch( - "policyengine.provenance.manifest.requests.get", - return_value=_response_with_json(payload), + with ( + patch( + "policyengine.provenance.manifest.get_release_manifest", + return_value=_legacy_us_country_manifest(), + ), + patch( + "policyengine.provenance.manifest.requests.get", + return_value=_response_with_json(payload), + ), ): certification = certify_data_release_compatibility( "us", @@ -720,12 +786,18 @@ def test__given_release_with_cut_tag__then_cut_tag_is_the_data_build_id(self): }, "compatible_model_packages": [], "default_datasets": {"national": "enhanced_cps_2024"}, - "artifacts": {}, + "artifacts": _legacy_us_artifacts("0.1.0"), } - with patch( - "policyengine.provenance.manifest.requests.get", - return_value=_response_with_json(payload), + with ( + patch( + "policyengine.provenance.manifest.get_release_manifest", + return_value=_legacy_us_country_manifest("0.1.0", "microcosm-data"), + ), + patch( + "policyengine.provenance.manifest.requests.get", + return_value=_response_with_json(payload), + ), ): certification = certify_data_release_compatibility( "us", @@ -914,12 +986,18 @@ def test__given_mismatched_version_and_fingerprint__then_certification_fails(sel }, "compatible_model_packages": [], "default_datasets": {"national": "enhanced_cps_2024"}, - "artifacts": {}, + "artifacts": _legacy_us_artifacts(), } - with patch( - "policyengine.provenance.manifest.requests.get", - return_value=_response_with_json(payload), + with ( + patch( + "policyengine.provenance.manifest.get_release_manifest", + return_value=_legacy_us_country_manifest(), + ), + patch( + "policyengine.provenance.manifest.requests.get", + return_value=_response_with_json(payload), + ), ): try: certify_data_release_compatibility( diff --git a/tests/test_release_tro_generation.py b/tests/test_release_tro_generation.py index d0aa9410d..fb4b6b33c 100644 --- a/tests/test_release_tro_generation.py +++ b/tests/test_release_tro_generation.py @@ -253,7 +253,7 @@ def test_strict_missing_remote_dataset_never_writes_limited_output(release, caps data = json.loads(release[1].content) del data["artifacts"][data["default_datasets"]["national"]] release[1]._content = json.dumps(data).encode() - with pytest.raises(ValueError, match="Default dataset"): + with pytest.raises(ValueError, match="missing default dataset artifact"): generator.regenerate_all(strict=True) assert path.read_bytes() == before assert "limited TRO" not in capsys.readouterr().err From 24bfd47d21d76f05d9daf6d7cd972a656b1c12e5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 05:37:30 -0400 Subject: [PATCH 5/8] Make local manifest activation conditional on verification --- changelog.d/703.added.md | 2 +- docs/bundles.md | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/changelog.d/703.added.md b/changelog.d/703.added.md index 9c630be9b..821b167b8 100644 --- a/changelog.d/703.added.md +++ b/changelog.d/703.added.md @@ -1 +1 @@ -Support opt-in local data release manifests pinned by the installed bundle TRO, with fail-closed verification and a distinct runtime certification basis for offline Budget-day runners. +Support opt-in local data release manifests pinned by the installed bundle TRO, with fail-closed verification and a distinct runtime certification basis for offline Budget-day runners. Reject conflicting TRO hash assertions and require local and fetched manifest artifacts to agree with the installed bundle. diff --git a/docs/bundles.md b/docs/bundles.md index c715ff6de..55fff09be 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -128,7 +128,11 @@ The trust anchor is the `trov:sha256` on `composition/1/artifact/data_release_manifest` in the installed country's `data/bundle/{country}.trace.tro.jsonld`. The file's **raw bytes** must match that pin. A missing pin, unreadable file, altered bytes, or certification mismatch -stops the process. The model, core, build ID and fingerprint checks still apply; +stops the process. Every definition of the artifact ID in the TRO must agree on +one digest. After revision normalization, the manifest's certified/default +artifact and all artifacts shared with the bundle must agree with the bundle's +locations and pinned hashes. This artifact check also applies to fetched +manifests. The model, core, build ID and fingerprint checks still apply; selecting a local file cannot extend the bundle's certification. At G0 each morning, while Hugging Face is available, use the exact installed @@ -137,7 +141,7 @@ enabling the setting. Keep the Hugging Face token in `HUGGING_FACE_TOKEN`, `HF_TOKEN` or `HUGGINGFACE_HUB_TOKEN`: ```bash -env -u POLICYENGINE_UK_DATA_MANIFEST_FILE POLICYENGINE_SKIP_COUNTRY_IMPORTS=1 python - <<'PY' +env -u POLICYENGINE_UK_DATA_MANIFEST_FILE POLICYENGINE_SKIP_COUNTRY_IMPORTS=1 python - <<'PY' && import hashlib from pathlib import Path @@ -174,11 +178,6 @@ receipt's certificate keeps the bundle's published basis so certificate verification remains reproducible. See [execution receipts](receipts.md). Refresh and verify the file whenever the installed certified release changes. -For Autumn Budget deployment, ship this support in the same `policyengine` -release as María's weekend Microcosm cut certification. Fetch the manifest for -that released bundle at G0; an earlier cut's manifest will fail its new pin. -Use the normal changelog-fragment release workflow for both changes. - ## Bundle-only PRs Run: From 0d7bf1a641d61b8951f47ab12d8d4867f57df140 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 05:49:34 -0400 Subject: [PATCH 6/8] Compare artifact identity fields without URI ambiguity --- src/policyengine/provenance/manifest.py | 37 ++++++++++++++++-------- tests/test_local_data_manifest.py | 38 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/policyengine/provenance/manifest.py b/src/policyengine/provenance/manifest.py index f06c4bc12..7729313ef 100644 --- a/src/policyengine/provenance/manifest.py +++ b/src/policyengine/provenance/manifest.py @@ -582,8 +582,21 @@ def refuse(dataset: str, detail: str) -> NoReturn: artifact = manifest.artifacts.get(certified.dataset) if artifact is None: refuse(certified.dataset, "missing certified dataset artifact") - if artifact.uri != certified.uri: - refuse(certified.dataset, f"expected {certified.uri}, got {artifact.uri}") + try: + location, revision = certified.uri.removeprefix("hf://").rsplit("@", 1) + owner, repo, path = location.split("/", 2) + except ValueError: + refuse(certified.dataset, "invalid bundled certified artifact URI") + expected_identity = (f"{owner}/{repo}", path, revision) + actual_identity = (artifact.repo_id, artifact.path, artifact.revision) + if ( + not certified.uri.startswith("hf://") + or actual_identity != expected_identity + ): + refuse( + certified.dataset, + f"expected {expected_identity!r}, got {actual_identity!r}", + ) if certified.sha256 is not None and artifact.sha256 != certified.sha256: refuse(certified.dataset, "certified SHA-256 pin differs") if default not in country.datasets and ( @@ -600,18 +613,18 @@ def refuse(dataset: str, detail: str) -> NoReturn: reference = country.datasets.get(name) if reference is None: continue - expected_uri = build_hf_uri( - repo_id=reference.repo_id or country.data_package.repo_id, - path_in_repo=reference.path, - revision=reference.revision or _artifact_revision(country.data_package), + expected_identity = ( + reference.repo_id or country.data_package.repo_id, + reference.path, + reference.revision or _artifact_revision(country.data_package), ) - actual_uri = build_hf_uri( - repo_id=artifact.repo_id, - path_in_repo=artifact_path_for_country_manifest(artifact, uri_parts), - revision=artifact.revision, + actual_identity = ( + artifact.repo_id, + artifact_path_for_country_manifest(artifact, uri_parts), + artifact.revision, ) - if actual_uri != expected_uri: - refuse(name, f"expected {expected_uri}, got {actual_uri}") + if actual_identity != expected_identity: + refuse(name, f"expected {expected_identity!r}, got {actual_identity!r}") if (artifact.repo_type or country.data_package.repo_type) != ( reference.repo_type or country.data_package.repo_type ): diff --git a/tests/test_local_data_manifest.py b/tests/test_local_data_manifest.py index 9b27e2938..b63cbdf14 100644 --- a/tests/test_local_data_manifest.py +++ b/tests/test_local_data_manifest.py @@ -556,6 +556,44 @@ def test_every_overlapping_managed_artifact_must_match( _certify() +@pytest.mark.parametrize("transport", ["local", "fetched"]) +@pytest.mark.parametrize("record", ["certified", "managed"]) +def test_artifact_repository_and_path_are_distinct_identity_fields( + tmp_path, monkeypatch, transport, record +): + payload = _payload() + name = "test_dataset" if record == "certified" else "supplement" + payload["artifacts"][name] = { + "kind": "microdata", + "repo_id": "policyengine/test-data/nested", + "path": "test_dataset.h5", + "revision": "f" * 40, + "sha256": "d" * 64, + } + local_path, trace_path, raw_bytes = _install_bundle(tmp_path, monkeypatch, payload) + + def edit(country): + if record == "certified": + country["datasets"].clear() + country["certified_data_artifact"]["uri"] = ( + f"hf://policyengine/test-data/nested/test_dataset.h5@{'f' * 40}" + ) + else: + country["datasets"][name] = { + "path": "nested/test_dataset.h5", + "revision": "f" * 40, + "sha256": "d" * 64, + } + + _edit_bundle(trace_path, edit) + if transport == "local": + monkeypatch.setenv(ENVIRONMENT_VARIABLE, str(local_path)) + with patch.object(manifests.requests, "get", return_value=_response(raw_bytes)): + # Concatenated URI strings agree, but the asserted repo/path do not. + with pytest.raises(ValueError, match="artifact.*installed bundle"): + _certify() + + @pytest.mark.parametrize("kind", ["diagnostics", "evidence"]) @pytest.mark.parametrize("prefixed", [False, True]) def test_release_relative_paths_match_certification_normalization( From da2ae8fb8e97145093e35366686df99c1e38449e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 05:50:51 -0400 Subject: [PATCH 7/8] Record PR 704 review fixes and validation handoff --- .job/HANDOFF.md | 57 +++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/.job/HANDOFF.md b/.job/HANDOFF.md index d2a91e2b6..d0029679a 100644 --- a/.job/HANDOFF.md +++ b/.job/HANDOFF.md @@ -1,47 +1,44 @@ -# Local manifest fallback handoff +# PR #704 review-fix handoff - Draft PR: https://github.com/PolicyEngine/policyengine.py/pull/704 -- Issue: https://github.com/PolicyEngine/policyengine.py/issues/703 -- Branch: `budget-local-data-manifest`, based on `origin/main` `485dc23302ec4abc6a5f7df5c6c03b8525f64c4d`. -- Reviewed implementation head: `78e09ed63c3805b8739bc9ea229a0d393c213e46` (this handoff is committed immediately afterwards). -- Commits: `0bb58755` local manifest/certification/tests; `78e09ed6` receipt/schema/docs/tests. +- Branch: `budget-local-data-manifest`; confirmed reviewed remote head `fd4e099ed3ff768041f3a537e6e38527c07b0bbf`. +- Validated implementation head SHA: `0d7bf1a641d61b8951f47ab12d8d4867f57df140`. +- The following commit updates only this handoff. Its exact final head is recorded in `.job/HEAD` and `.job/local-data-manifest.bundle`. -## Design +## Fixes -`POLICYENGINE__DATA_MANIFEST_FILE` always selects the local file when present. This avoids outage latency and gives deterministic behavior. Unset keeps the original cached network fetch and certification results. - -The pin is the raw-byte `trov:sha256` for `composition/1/artifact/data_release_manifest` in the installed country TRACE TRO. Its arrangement must name the installed release's manifest URI. Missing, malformed, duplicate, or stale pins refuse. The current installed UK pin was checked as `49f9fa820db2e6544ff5cdceeb2d868a504843a6a1c1c06b37188621e700066f`. No environment digest is accepted. Local bytes are re-read and hashed on every call, before JSON parsing/revision rewriting, so cached remote/local loads cannot bypass verification. Invalid paths and digest errors fail closed with digest context. - -Local bytes run through the unchanged certification acceptance logic. All four successful runtime bases gain `_local_manifest`; the claim-based set includes the corresponding local variants. Exact-pair model/core/build restrictions and fingerprint acceptance remain the same. TRACE accepts the basis as a string. Packaged receipts recheck local certification and record it in optional `runtime.data_certification_basis`, keeping the published bundle certificate basis and both digest domains unchanged. Unset receipts omit the new annotation. The schema is regenerated and checked. Caller-supplied bundles do not claim packaged provenance. +- Scan every TRO definition of the manifest artifact ID, including direct graph nodes, nested definitions and artifact references; require one distinct valid SHA-256 assertion. Preserve canonical inline/location checks. +- Validate both local and fetched manifest artifacts after revision rewriting. The certified/default artifact must be present and anchored; every overlapping managed artifact must agree with bundled repo/path/revision, repository type and pinned SHA-256. Compare identity fields separately to prevent ambiguous URI concatenation. Primary manifests may omit separately pinned regional artifacts and overlays. +- Share the existing release-relative diagnostics/evidence normalization with certification. Exact-pair model/core/build and fingerprint acceptance logic remains unchanged; unset fetch/cache/offline behavior is preserved for consistent bundles. +- Replace function attributes with a typed callable cache facade. +- Make the documented export conditional on successful download/verification/atomic replacement; remove release coordination from permanent public docs. +- PR body records the invariants and both downstream actions. No other repository was edited. ## Validation -- 38 local-manifest tests passed, including 80 Hypothesis byte mutations and 10 direct main-snapshot differential scenarios. -- 146 existing certification tests passed. -- 96 bundle/bootstrap/tooling tests passed. -- Broad provenance suite: 2,337 passed, 6 skipped, including all 6 new receipt tests. -- Wheel/package-pin tests: 96 passed. -- Total across these final focused runs: **2,713 passed, 6 skipped**. Skips cover private data, Python/model-version-specific cases, and disabled top-level country imports in the broad run. -- `make format`, `make lint`, `git diff --check`, and `scripts/generate_receipt_schemas.py --check` passed. -- Every pytest invocation checked load1 <=110 and `os.statvfs` available bytes >=55,000,000,000. Observed load was below 37 and free disk above 132 GB. No pytest workers were used. -- Local fixtures are synthetic; no private manifest or microdata is committed. +**3,322 unique cases passed; five skipped.** -Detailed parent-run logs are local files under `.job/*tests.log`. +- Initial local/TRO-pin run: 88 passed, including two 80-example Hypothesis properties (byte mutation and duplicate placement). +- Broad 56-module provenance/certification/disclosure run: 3,256 passed, five skipped, one failure from the old missing-default error-message assertion. It now refuses earlier in the shared validator; the corrected complete TRO regeneration module passed all 40 cases on rerun. +- Release/bootstrap-focused run: 189 passed, including 59 cases outside the broad selection and two new default-anchor cases. +- Final affected manifest/certification run: 280 passed, including all 82 local-manifest cases, all 12 TRO-pin cases and four new repo/path-boundary cases. No failures remain in pytest's failure cache. +- `make format`, `make lint`, focused `mypy --follow-imports=silent src/policyengine/provenance/manifest.py`, `git diff --check`, and generated receipt schema checks passed. Documentation shell block passes `bash -n`. +- Every pytest invocation checked load1 <=110 and available disk >=55,000,000,000 bytes; no pytest workers. Observed load1 9.04–19.02; available disk 144.3–146.9 GB decimal. Tests used Python 3.13.9. +- Detailed logs and selected modules remain under `.job/`. No private manifests or microdata were committed. -## Coordination and remaining work +## Downstream actions -- Leave PR draft; the hub reviews and merges it. -- Ship in the same policyengine.py release as María's weekend Microcosm cut certification. No versions or country/data pins were changed. `changelog.d/703.added.md` follows the release workflow. -- The G0 morning fetch/verify/atomic-refresh instructions are in `docs/bundles.md`. Refresh the local manifest after adopting the newly certified release. -- Dashboard follow-up: refresh `tests/fixtures/w4/execution_receipt.v1.schema.json` in `PolicyEngine/uk-autumn-budget-dashboard-2026` when upgrading; its receipt adapter has no basis allowlist, but its schema equality test requires the new schema. See `docs/receipts.md`. +- Dashboard: refresh vendored `tests/fixtures/w4/execution_receipt.v1.schema.json` when upgrading. +- Scorecard: `pipeline/uk_replay_variants.py:730` retains the setting in offline isolation, while `:750` requires exact runtime bundle equality. Clear `POLICYENGINE_*_DATA_MANIFEST_FILE` during isolation. +- Leave the PR draft; do not merge. No versions, country pins, data or certified releases changed. -## Workspace Git metadata +## Workspace Git metadata and delivery -The assigned worktree's protected `.git` points into the caller's repository, where metadata writes are sandbox-blocked. All work and independent Git metadata stayed in this workspace. To inspect the actual branch here: +The protected workspace `.git` points into the caller's repository. All mutable Git metadata is instead inside this workspace: ```bash GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git status -GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git log -3 --oneline +GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git log -4 --oneline ``` -The branch is pushed to the canonical repository. A portable `.job/local-data-manifest.bundle` is also left for harvesting; it contains `budget-local-data-manifest` relative to the main base. The original protected `.git` pointer was not changed. No caller files were written. +The original pointer and caller checkout were not changed. `.job/local-data-manifest.bundle` is portable relative to main `485dc23302ec4abc6a5f7df5c6c03b8525f64c4d`. The single final push is attempted after committing this handoff; its outcome is recorded in `.job/PUSH_RESULT.txt`. `.job/HEAD` contains the final branch SHA. No history was rewritten and nothing was pushed to main or master. From 463c3a953ccaeb110e3e5a7f7c494ed46597d52d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 13:44:49 -0400 Subject: [PATCH 8/8] Remove the build job's hand-off note from the PR The note was a Subfleet job artifact, not part of the change. The hub keeps a copy outside the repository. Co-Authored-By: Claude Opus 5.5 --- .job/HANDOFF.md | 44 -------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .job/HANDOFF.md diff --git a/.job/HANDOFF.md b/.job/HANDOFF.md deleted file mode 100644 index d0029679a..000000000 --- a/.job/HANDOFF.md +++ /dev/null @@ -1,44 +0,0 @@ -# PR #704 review-fix handoff - -- Draft PR: https://github.com/PolicyEngine/policyengine.py/pull/704 -- Branch: `budget-local-data-manifest`; confirmed reviewed remote head `fd4e099ed3ff768041f3a537e6e38527c07b0bbf`. -- Validated implementation head SHA: `0d7bf1a641d61b8951f47ab12d8d4867f57df140`. -- The following commit updates only this handoff. Its exact final head is recorded in `.job/HEAD` and `.job/local-data-manifest.bundle`. - -## Fixes - -- Scan every TRO definition of the manifest artifact ID, including direct graph nodes, nested definitions and artifact references; require one distinct valid SHA-256 assertion. Preserve canonical inline/location checks. -- Validate both local and fetched manifest artifacts after revision rewriting. The certified/default artifact must be present and anchored; every overlapping managed artifact must agree with bundled repo/path/revision, repository type and pinned SHA-256. Compare identity fields separately to prevent ambiguous URI concatenation. Primary manifests may omit separately pinned regional artifacts and overlays. -- Share the existing release-relative diagnostics/evidence normalization with certification. Exact-pair model/core/build and fingerprint acceptance logic remains unchanged; unset fetch/cache/offline behavior is preserved for consistent bundles. -- Replace function attributes with a typed callable cache facade. -- Make the documented export conditional on successful download/verification/atomic replacement; remove release coordination from permanent public docs. -- PR body records the invariants and both downstream actions. No other repository was edited. - -## Validation - -**3,322 unique cases passed; five skipped.** - -- Initial local/TRO-pin run: 88 passed, including two 80-example Hypothesis properties (byte mutation and duplicate placement). -- Broad 56-module provenance/certification/disclosure run: 3,256 passed, five skipped, one failure from the old missing-default error-message assertion. It now refuses earlier in the shared validator; the corrected complete TRO regeneration module passed all 40 cases on rerun. -- Release/bootstrap-focused run: 189 passed, including 59 cases outside the broad selection and two new default-anchor cases. -- Final affected manifest/certification run: 280 passed, including all 82 local-manifest cases, all 12 TRO-pin cases and four new repo/path-boundary cases. No failures remain in pytest's failure cache. -- `make format`, `make lint`, focused `mypy --follow-imports=silent src/policyengine/provenance/manifest.py`, `git diff --check`, and generated receipt schema checks passed. Documentation shell block passes `bash -n`. -- Every pytest invocation checked load1 <=110 and available disk >=55,000,000,000 bytes; no pytest workers. Observed load1 9.04–19.02; available disk 144.3–146.9 GB decimal. Tests used Python 3.13.9. -- Detailed logs and selected modules remain under `.job/`. No private manifests or microdata were committed. - -## Downstream actions - -- Dashboard: refresh vendored `tests/fixtures/w4/execution_receipt.v1.schema.json` when upgrading. -- Scorecard: `pipeline/uk_replay_variants.py:730` retains the setting in offline isolation, while `:750` requires exact runtime bundle equality. Clear `POLICYENGINE_*_DATA_MANIFEST_FILE` during isolation. -- Leave the PR draft; do not merge. No versions, country pins, data or certified releases changed. - -## Workspace Git metadata and delivery - -The protected workspace `.git` points into the caller's repository. All mutable Git metadata is instead inside this workspace: - -```bash -GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git status -GIT_DIR="$PWD/.job/git-copy/.git" GIT_WORK_TREE="$PWD" git log -4 --oneline -``` - -The original pointer and caller checkout were not changed. `.job/local-data-manifest.bundle` is portable relative to main `485dc23302ec4abc6a5f7df5c6c03b8525f64c4d`. The single final push is attempted after committing this handoff; its outcome is recorded in `.job/PUSH_RESULT.txt`. `.job/HEAD` contains the final branch SHA. No history was rewritten and nothing was pushed to main or master.