From 0cac905538aa51831157626d026146bd97236db0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:04:18 +0000 Subject: [PATCH 01/15] chore(deps): bump ruff from 0.16.1 to 0.16.3 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.1 to 0.16.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.16.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-ci.txt | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/requirements-ci.txt b/requirements-ci.txt index 267acdae..fffc84b7 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -34,8 +34,25 @@ pytest==9.1.1 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c pytest-asyncio==1.4.0 \ --hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 -ruff==0.16.1 \ - --hash=sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c +ruff==0.16.3 \ + --hash=sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b \ + --hash=sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7 \ + --hash=sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb \ + --hash=sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413 \ + --hash=sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a \ + --hash=sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474 \ + --hash=sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82 \ + --hash=sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84 \ + --hash=sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da \ + --hash=sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d \ + --hash=sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870 \ + --hash=sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a \ + --hash=sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506 \ + --hash=sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9 \ + --hash=sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2 \ + --hash=sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948 \ + --hash=sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50 \ + --hash=sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081 tomli==2.4.1 ; python_version < "3.11" \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe typing-extensions==4.16.0 ; python_version < "3.13" \ From 006161e9530ae081623f81b6cf4d32bce6a55bcc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:22:38 -0700 Subject: [PATCH 02/15] ci: repair multi-hash lock evidence for Ruff update --- .github/workflows/repair-pr-224.yml | 313 ++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 .github/workflows/repair-pr-224.yml diff --git a/.github/workflows/repair-pr-224.yml b/.github/workflows/repair-pr-224.yml new file mode 100644 index 00000000..7b39bfd4 --- /dev/null +++ b/.github/workflows/repair-pr-224.yml @@ -0,0 +1,313 @@ +name: Repair PR 224 multi-hash lock evidence + +on: + push: + branches: + - dependabot/pip/ruff-0.16.3 + paths: + - .github/workflows/repair-pr-224.yml + +permissions: + contents: write + +concurrency: + group: repair-pr-224-${{ github.ref }} + cancel-in-progress: false + +jobs: + repair: + if: >- + github.repository == 'ContextualWisdomLab/EgressWeave' && + github.ref == 'refs/heads/dependabot/pip/ruff-0.16.3' + runs-on: ubuntu-24.04 + timeout-minutes: 35 + env: + EXPECTED_PARENT_SHA: 0cac905538aa51831157626d026146bd97236db0 + TARGET_BRANCH: dependabot/pip/ruff-0.16.3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + fetch-depth: 2 + persist-credentials: false + - name: Verify exact repair boundary + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT_SHA" + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test "$(git branch --show-current)" = "$TARGET_BRANCH" + - name: Apply minimal multi-hash parser correction and RED regressions + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + script_path = Path("scripts/ci/generate_release_sbom.py") + script = script_path.read_text(encoding="utf-8") + old_loader = '''def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]: + """Load exact package versions, markers, and hashes from the CI lock.""" + try: + if path.stat().st_size > MAX_MANIFEST_BYTES: + raise SystemExit("runtime lock exceeds the safety bound") + content = path.read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + raise SystemExit("runtime lock is unreadable") from error + entries: dict[str, dict[str, str | None]] = {} + for raw_line in content.replace("\\\\\\n", " ").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if line.count("--hash=sha256:") != 1: + raise SystemExit("runtime lock entries require exactly one SHA-256 hash") + requirement_text, digest = line.split("--hash=sha256:", 1) + digest = digest.strip() + if SHA256.fullmatch(digest) is None: + raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + try: + requirement = Requirement(requirement_text.strip()) + except InvalidRequirement as error: + raise SystemExit("runtime lock contains an invalid requirement") from error + if requirement.extras: + raise SystemExit("runtime lock requirements must not use extras") + name = _name(requirement.name) + if name in entries: + raise SystemExit(f"runtime lock duplicates package {name!r}") + specifiers = list(requirement.specifier) + version = ( + specifiers[0].version + if requirement.url is None + and len(specifiers) == 1 + and specifiers[0].operator == "==" + else None + ) + entries[name] = { + "version": version, + "sha256": digest, + "marker": str(requirement.marker) if requirement.marker is not None else None, + } + return entries + ''' + new_loader = '''def _load_runtime_lock( + path: Path, + ) -> dict[str, dict[str, str | tuple[str, ...] | None]]: + """Load exact package versions, markers, and allowed hashes from the CI lock.""" + try: + if path.stat().st_size > MAX_MANIFEST_BYTES: + raise SystemExit("runtime lock exceeds the safety bound") + content = path.read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + raise SystemExit("runtime lock is unreadable") from error + entries: dict[str, dict[str, str | tuple[str, ...] | None]] = {} + for raw_line in content.replace("\\\\\\n", " ").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + segments = line.split("--hash=sha256:") + if len(segments) < 2: + raise SystemExit("runtime lock entries require at least one SHA-256 hash") + requirement_text = segments[0] + digests = tuple(segment.strip() for segment in segments[1:]) + if any(SHA256.fullmatch(digest) is None for digest in digests): + raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + if len(set(digests)) != len(digests): + raise SystemExit("runtime lock contains a duplicate SHA-256 hash") + try: + requirement = Requirement(requirement_text.strip()) + except InvalidRequirement as error: + raise SystemExit("runtime lock contains an invalid requirement") from error + if requirement.extras: + raise SystemExit("runtime lock requirements must not use extras") + name = _name(requirement.name) + if name in entries: + raise SystemExit(f"runtime lock duplicates package {name!r}") + specifiers = list(requirement.specifier) + version = ( + specifiers[0].version + if requirement.url is None + and len(specifiers) == 1 + and specifiers[0].operator == "==" + else None + ) + entries[name] = { + "version": version, + "sha256": digests, + "marker": str(requirement.marker) if requirement.marker is not None else None, + } + return entries + ''' + if script.count(old_loader) != 1: + raise SystemExit("runtime-lock loader boundary drifted") + script = script.replace(old_loader, new_loader, 1) + + old_validator = ''' for name, component in components.items(): + locked = lock_entries.get(name) + expected = { + "version": component["version"], + "sha256": component["sha256"], + "marker": _canonical_marker(component["marker"], f"component {name}"), + } + if locked != expected: + raise SystemExit( + f"component {name!r} does not match the hash-locked runtime subset" + ) + ''' + new_validator = ''' for name, component in components.items(): + locked = lock_entries.get(name) + expected_marker = _canonical_marker( + component["marker"], f"component {name}" + ) + if ( + locked is None + or locked["version"] != component["version"] + or locked["marker"] != expected_marker + or component["sha256"] not in locked["sha256"] + ): + raise SystemExit( + f"component {name!r} does not match the hash-locked runtime subset" + ) + ''' + if script.count(old_validator) != 1: + raise SystemExit("runtime-lock validation boundary drifted") + script_path.write_text( + script.replace(old_validator, new_validator, 1), + encoding="utf-8", + ) + + Path("tests/test_release_lock_hash_sets.py").write_text( + '''"""Regression contracts for portable multi-artifact hash locks.""" + + from __future__ import annotations + + import importlib.util + from pathlib import Path + + import pytest + + REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" + MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" + LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" + + + def _load_generator(): + """Load the repository SBOM generator without packaging it.""" + specification = importlib.util.spec_from_file_location( + "egressweave_release_lock_hash_sets", + GENERATOR_PATH, + ) + assert specification is not None and specification.loader is not None + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module + + + def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: + """Preserve every exact artifact hash attached to one pinned version.""" + generator = _load_generator() + first_digest = "a" * 64 + second_digest = "b" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + "tool==1.2.3 \\\\n" + f" --hash=sha256:{first_digest} \\\\n" + f" --hash=sha256:{second_digest}\\n", + encoding="utf-8", + ) + + assert generator._load_runtime_lock(lock_path) == { + "tool": { + "version": "1.2.3", + "sha256": (first_digest, second_digest), + "marker": None, + } + } + + + def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: + """Reject ambiguous duplicate evidence within one package entry.""" + generator = _load_generator() + digest = "a" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n", + encoding="utf-8", + ) + + with pytest.raises(SystemExit, match="duplicate SHA-256"): + generator._load_runtime_lock(lock_path) + + + def test_runtime_manifest_accepts_its_digest_among_platform_hashes( + tmp_path: Path, + ) -> None: + """Bind runtime evidence to one reviewed digest in a portable hash set.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + marker = "anyio==4.14.2 \\\\n --hash=sha256:" + assert lock_text.count(marker) == 1 + prefix, suffix = lock_text.split(marker, 1) + digest, remainder = suffix.split("\\n", 1) + portable_lock = tmp_path / "requirements-ci.txt" + portable_lock.write_text( + prefix + + marker + + digest + + " \\\\n --hash=sha256:" + + ("f" * 64) + + "\\n" + + remainder, + encoding="utf-8", + ) + + generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) + ''', + encoding="utf-8", + ) + + changelog_path = Path("CHANGELOG.md") + changelog = changelog_path.read_text(encoding="utf-8") + anchor = "### Fixed\n" + addition = ( + "- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while " + "requiring every reviewed runtime SBOM digest to be present in that immutable set. " + "This preserves portable hash-locked tooling without weakening release evidence.\n" + ) + if changelog.count(anchor) != 1 or addition in changelog: + raise SystemExit("changelog insertion boundary drifted") + changelog_path.write_text( + changelog.replace(anchor, anchor + addition, 1), + encoding="utf-8", + ) + PY + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + cache: pip + cache-dependency-path: requirements-ci.txt + - name: Install immutable CI toolchain + run: python -m pip install --require-hashes -r requirements-ci.txt + - name: Verify focused and complete GREEN + shell: bash + run: | + set -euo pipefail + ruff check . + coverage erase + coverage run -m pytest -q + coverage report -m + python -m compileall -q src tests scripts + git diff --check + - name: Publish verified repair and remove temporary authority + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + rm .github/workflows/repair-pr-224.yml + git add -A + git config user.name "CWL One-shot Repair" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "fix(sbom): accept portable multi-hash lock entries" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git push origin "HEAD:refs/heads/${TARGET_BRANCH}" From 6ebfe145363229f06ee34898c75bd9ede2c6c6ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:24:14 -0700 Subject: [PATCH 03/15] ci: correct the multi-hash repair boundary --- .github/workflows/repair-pr-224.yml | 282 ++++++++++++---------------- 1 file changed, 115 insertions(+), 167 deletions(-) diff --git a/.github/workflows/repair-pr-224.yml b/.github/workflows/repair-pr-224.yml index 7b39bfd4..3e606270 100644 --- a/.github/workflows/repair-pr-224.yml +++ b/.github/workflows/repair-pr-224.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 35 env: - EXPECTED_PARENT_SHA: 0cac905538aa51831157626d026146bd97236db0 + EXPECTED_PARENT_SHA: 006161e9530ae081623f81b6cf4d32bce6a55bcc TARGET_BRANCH: dependabot/pip/ruff-0.16.3 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -37,111 +37,58 @@ jobs: test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT_SHA" test "$(git rev-parse HEAD)" = "$GITHUB_SHA" test "$(git branch --show-current)" = "$TARGET_BRANCH" - - name: Apply minimal multi-hash parser correction and RED regressions + - name: Apply minimal multi-hash parser correction and regressions shell: bash run: | set -euo pipefail python - <<'PY' from pathlib import Path + from textwrap import dedent script_path = Path("scripts/ci/generate_release_sbom.py") script = script_path.read_text(encoding="utf-8") - old_loader = '''def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]: - """Load exact package versions, markers, and hashes from the CI lock.""" - try: - if path.stat().st_size > MAX_MANIFEST_BYTES: - raise SystemExit("runtime lock exceeds the safety bound") - content = path.read_text(encoding="utf-8") - except (OSError, UnicodeError) as error: - raise SystemExit("runtime lock is unreadable") from error - entries: dict[str, dict[str, str | None]] = {} - for raw_line in content.replace("\\\\\\n", " ").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - if line.count("--hash=sha256:") != 1: + + replacements = [ + ( + "def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n", + "def _load_runtime_lock(\n" + " path: Path,\n" + ") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n", + ), + ( + ' """Load exact package versions, markers, and hashes from the CI lock."""\n', + ' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n', + ), + ( + " entries: dict[str, dict[str, str | None]] = {}\n", + " entries: dict[\n" + " str, dict[str, str | tuple[str, ...] | None]\n" + " ] = {}\n", + ), + ( + ''' if line.count("--hash=sha256:") != 1: raise SystemExit("runtime lock entries require exactly one SHA-256 hash") requirement_text, digest = line.split("--hash=sha256:", 1) digest = digest.strip() if SHA256.fullmatch(digest) is None: raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - try: - requirement = Requirement(requirement_text.strip()) - except InvalidRequirement as error: - raise SystemExit("runtime lock contains an invalid requirement") from error - if requirement.extras: - raise SystemExit("runtime lock requirements must not use extras") - name = _name(requirement.name) - if name in entries: - raise SystemExit(f"runtime lock duplicates package {name!r}") - specifiers = list(requirement.specifier) - version = ( - specifiers[0].version - if requirement.url is None - and len(specifiers) == 1 - and specifiers[0].operator == "==" - else None - ) - entries[name] = { - "version": version, - "sha256": digest, - "marker": str(requirement.marker) if requirement.marker is not None else None, - } - return entries - ''' - new_loader = '''def _load_runtime_lock( - path: Path, - ) -> dict[str, dict[str, str | tuple[str, ...] | None]]: - """Load exact package versions, markers, and allowed hashes from the CI lock.""" - try: - if path.stat().st_size > MAX_MANIFEST_BYTES: - raise SystemExit("runtime lock exceeds the safety bound") - content = path.read_text(encoding="utf-8") - except (OSError, UnicodeError) as error: - raise SystemExit("runtime lock is unreadable") from error - entries: dict[str, dict[str, str | tuple[str, ...] | None]] = {} - for raw_line in content.replace("\\\\\\n", " ").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - segments = line.split("--hash=sha256:") + ''', + ''' segments = line.split("--hash=sha256:") if len(segments) < 2: - raise SystemExit("runtime lock entries require at least one SHA-256 hash") + raise SystemExit( + "runtime lock entries require at least one SHA-256 hash" + ) requirement_text = segments[0] digests = tuple(segment.strip() for segment in segments[1:]) if any(SHA256.fullmatch(digest) is None for digest in digests): raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") if len(set(digests)) != len(digests): raise SystemExit("runtime lock contains a duplicate SHA-256 hash") - try: - requirement = Requirement(requirement_text.strip()) - except InvalidRequirement as error: - raise SystemExit("runtime lock contains an invalid requirement") from error - if requirement.extras: - raise SystemExit("runtime lock requirements must not use extras") - name = _name(requirement.name) - if name in entries: - raise SystemExit(f"runtime lock duplicates package {name!r}") - specifiers = list(requirement.specifier) - version = ( - specifiers[0].version - if requirement.url is None - and len(specifiers) == 1 - and specifiers[0].operator == "==" - else None - ) - entries[name] = { - "version": version, - "sha256": digests, - "marker": str(requirement.marker) if requirement.marker is not None else None, - } - return entries - ''' - if script.count(old_loader) != 1: - raise SystemExit("runtime-lock loader boundary drifted") - script = script.replace(old_loader, new_loader, 1) - - old_validator = ''' for name, component in components.items(): + ''', + ), + (' "sha256": digest,\n', ' "sha256": digests,\n'), + ( + ''' for name, component in components.items(): locked = lock_entries.get(name) expected = { "version": component["version"], @@ -152,8 +99,8 @@ jobs: raise SystemExit( f"component {name!r} does not match the hash-locked runtime subset" ) - ''' - new_validator = ''' for name, component in components.items(): + ''', + ''' for name, component in components.items(): locked = lock_entries.get(name) expected_marker = _canonical_marker( component["marker"], f"component {name}" @@ -167,102 +114,103 @@ jobs: raise SystemExit( f"component {name!r} does not match the hash-locked runtime subset" ) - ''' - if script.count(old_validator) != 1: - raise SystemExit("runtime-lock validation boundary drifted") - script_path.write_text( - script.replace(old_validator, new_validator, 1), - encoding="utf-8", - ) + ''', + ), + ] + for old, new in replacements: + if script.count(old) != 1: + raise SystemExit(f"runtime-lock repair boundary drifted: {old[:60]!r}") + script = script.replace(old, new, 1) + script_path.write_text(script, encoding="utf-8") Path("tests/test_release_lock_hash_sets.py").write_text( - '''"""Regression contracts for portable multi-artifact hash locks.""" + dedent( + '''\ + """Regression contracts for portable multi-artifact hash locks.""" - from __future__ import annotations + from __future__ import annotations - import importlib.util - from pathlib import Path + import importlib.util + from pathlib import Path - import pytest + import pytest - REPOSITORY_ROOT = Path(__file__).resolve().parents[1] - GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" - MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" - LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" + REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" + MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" + LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" - def _load_generator(): - """Load the repository SBOM generator without packaging it.""" - specification = importlib.util.spec_from_file_location( - "egressweave_release_lock_hash_sets", - GENERATOR_PATH, - ) - assert specification is not None and specification.loader is not None - module = importlib.util.module_from_spec(specification) - specification.loader.exec_module(module) - return module + def _load_generator(): + """Load the repository SBOM generator without packaging it.""" + specification = importlib.util.spec_from_file_location( + "egressweave_release_lock_hash_sets", + GENERATOR_PATH, + ) + assert specification is not None and specification.loader is not None + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module - def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: - """Preserve every exact artifact hash attached to one pinned version.""" - generator = _load_generator() - first_digest = "a" * 64 - second_digest = "b" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - "tool==1.2.3 \\\\n" - f" --hash=sha256:{first_digest} \\\\n" - f" --hash=sha256:{second_digest}\\n", - encoding="utf-8", - ) + def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: + """Preserve every exact artifact hash attached to one pinned version.""" + generator = _load_generator() + first_digest = "a" * 64 + second_digest = "b" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + "tool==1.2.3 " + f"--hash=sha256:{first_digest} " + f"--hash=sha256:{second_digest}\\n", + encoding="utf-8", + ) - assert generator._load_runtime_lock(lock_path) == { - "tool": { - "version": "1.2.3", - "sha256": (first_digest, second_digest), - "marker": None, - } - } + assert generator._load_runtime_lock(lock_path) == { + "tool": { + "version": "1.2.3", + "sha256": (first_digest, second_digest), + "marker": None, + } + } - def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: - """Reject ambiguous duplicate evidence within one package entry.""" - generator = _load_generator() - digest = "a" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n", - encoding="utf-8", - ) + def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: + """Reject ambiguous duplicate evidence within one package entry.""" + generator = _load_generator() + digest = "a" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n", + encoding="utf-8", + ) - with pytest.raises(SystemExit, match="duplicate SHA-256"): - generator._load_runtime_lock(lock_path) + with pytest.raises(SystemExit, match="duplicate SHA-256"): + generator._load_runtime_lock(lock_path) - def test_runtime_manifest_accepts_its_digest_among_platform_hashes( - tmp_path: Path, - ) -> None: - """Bind runtime evidence to one reviewed digest in a portable hash set.""" - generator = _load_generator() - lock_text = LOCK_PATH.read_text(encoding="utf-8") - marker = "anyio==4.14.2 \\\\n --hash=sha256:" - assert lock_text.count(marker) == 1 - prefix, suffix = lock_text.split(marker, 1) - digest, remainder = suffix.split("\\n", 1) - portable_lock = tmp_path / "requirements-ci.txt" - portable_lock.write_text( - prefix - + marker - + digest - + " \\\\n --hash=sha256:" - + ("f" * 64) - + "\\n" - + remainder, - encoding="utf-8", - ) + def test_runtime_manifest_accepts_its_digest_among_platform_hashes( + tmp_path: Path, + ) -> None: + """Bind runtime evidence to one reviewed digest in a portable hash set.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + needle = ( + "anyio==4.14.2 \\\\n" + " --hash=sha256:" + "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" + ) + replacement = needle + " \\\\n --hash=sha256:" + ("f" * 64) + assert lock_text.count(needle) == 1 + portable_lock = tmp_path / "requirements-ci.txt" + portable_lock.write_text( + lock_text.replace(needle, replacement, 1), + encoding="utf-8", + ) - generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) - ''', + generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) + ''' + ), encoding="utf-8", ) From 52f1885d45f3126e2f7abbd9558a34cc1bfb3a39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:31:09 -0700 Subject: [PATCH 04/15] ci: run verified Ruff repair from a trusted branch --- .github/workflows/repair-pr-224.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/repair-pr-224.yml b/.github/workflows/repair-pr-224.yml index 3e606270..34c51dae 100644 --- a/.github/workflows/repair-pr-224.yml +++ b/.github/workflows/repair-pr-224.yml @@ -3,7 +3,7 @@ name: Repair PR 224 multi-hash lock evidence on: push: branches: - - dependabot/pip/ruff-0.16.3 + - repair/ruff-0.16.3-multihash-lock paths: - .github/workflows/repair-pr-224.yml @@ -18,12 +18,12 @@ jobs: repair: if: >- github.repository == 'ContextualWisdomLab/EgressWeave' && - github.ref == 'refs/heads/dependabot/pip/ruff-0.16.3' + github.ref == 'refs/heads/repair/ruff-0.16.3-multihash-lock' runs-on: ubuntu-24.04 timeout-minutes: 35 env: - EXPECTED_PARENT_SHA: 006161e9530ae081623f81b6cf4d32bce6a55bcc - TARGET_BRANCH: dependabot/pip/ruff-0.16.3 + EXPECTED_PARENT_SHA: 6ebfe145363229f06ee34898c75bd9ede2c6c6ac + TARGET_BRANCH: repair/ruff-0.16.3-multihash-lock steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -196,11 +196,11 @@ jobs: generator = _load_generator() lock_text = LOCK_PATH.read_text(encoding="utf-8") needle = ( - "anyio==4.14.2 \\\\n" + "anyio==4.14.2 \\\n" " --hash=sha256:" "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" ) - replacement = needle + " \\\\n --hash=sha256:" + ("f" * 64) + replacement = needle + " \\\n --hash=sha256:" + ("f" * 64) assert lock_text.count(needle) == 1 portable_lock = tmp_path / "requirements-ci.txt" portable_lock.write_text( From 34716768213e9455a0e1a8f5bcfe75c59659adaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:14:51 -0700 Subject: [PATCH 05/15] ci: verify portable multi-hash lock repair --- .github/workflows/repair-pr-226-final.yml | 312 ++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 .github/workflows/repair-pr-226-final.yml diff --git a/.github/workflows/repair-pr-226-final.yml b/.github/workflows/repair-pr-226-final.yml new file mode 100644 index 00000000..d526b1ec --- /dev/null +++ b/.github/workflows/repair-pr-226-final.yml @@ -0,0 +1,312 @@ +name: Repair PR 226 portable hash-lock evidence + +on: + push: + branches: + - repair/ruff-0.16.3-multihash-lock + paths: + - .github/workflows/repair-pr-226-final.yml + +permissions: + contents: read + +concurrency: + group: repair-pr-226-final-${{ github.ref }} + cancel-in-progress: false + +jobs: + repair: + if: >- + github.repository == 'ContextualWisdomLab/EgressWeave' && + github.ref == 'refs/heads/repair/ruff-0.16.3-multihash-lock' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + env: + EXPECTED_PARENT_SHA: 52f1885d45f3126e2f7abbd9558a34cc1bfb3a39 + TARGET_BRANCH: repair/ruff-0.16.3-multihash-lock + steps: + - name: Check out exact repair trigger + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + fetch-depth: 2 + persist-credentials: false + + - name: Verify exact single-writer boundary + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT_SHA" + test "$(git status --porcelain)" = "" + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + cache: pip + cache-dependency-path: requirements-ci.txt + + - name: Install immutable CI toolchain + run: python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-ci.txt + + - name: Add RED portable-lock regressions + shell: bash + run: | + set -euo pipefail + cat > tests/test_release_lock_hash_sets.py <<'PY' + """Regression contracts for portable multi-artifact hash locks.""" + + from __future__ import annotations + + import importlib.util + from pathlib import Path + + import pytest + + REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" + MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" + LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" + + + def _load_generator(): + """Load the repository SBOM generator without packaging it.""" + specification = importlib.util.spec_from_file_location( + "egressweave_release_lock_hash_sets", + GENERATOR_PATH, + ) + assert specification is not None and specification.loader is not None + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module + + + def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: + """Preserve every exact artifact hash attached to one pinned version.""" + generator = _load_generator() + first_digest = "a" * 64 + second_digest = "b" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + "tool==1.2.3 " + f"--hash=sha256:{first_digest} " + f"--hash=sha256:{second_digest}\n", + encoding="utf-8", + ) + + assert generator._load_runtime_lock(lock_path) == { + "tool": { + "version": "1.2.3", + "sha256": (first_digest, second_digest), + "marker": None, + } + } + + + def test_runtime_lock_keeps_one_hash_as_a_hash_set(tmp_path: Path) -> None: + """Represent a one-artifact lock with the same immutable tuple contract.""" + generator = _load_generator() + digest = "a" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{digest}\n", + encoding="utf-8", + ) + + assert generator._load_runtime_lock(lock_path)["tool"]["sha256"] == ( + digest, + ) + + + def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: + """Reject ambiguous duplicate evidence within one package entry.""" + generator = _load_generator() + digest = "a" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\n", + encoding="utf-8", + ) + + with pytest.raises(SystemExit, match="duplicate SHA-256"): + generator._load_runtime_lock(lock_path) + + + def test_runtime_manifest_accepts_its_digest_among_platform_hashes( + tmp_path: Path, + ) -> None: + """Bind runtime evidence to one reviewed digest in a portable hash set.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + needle = ( + "anyio==4.14.2 \\\n" + " --hash=sha256:" + "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" + ) + replacement = needle + " \\\n --hash=sha256:" + ("f" * 64) + assert lock_text.count(needle) == 1 + portable_lock = tmp_path / "requirements-ci.txt" + portable_lock.write_text( + lock_text.replace(needle, replacement, 1), + encoding="utf-8", + ) + + generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) + + + def test_runtime_manifest_rejects_digest_outside_reviewed_hash_set( + tmp_path: Path, + ) -> None: + """Reject a runtime artifact digest absent from its immutable lock set.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + reviewed = "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" + assert lock_text.count(reviewed) == 1 + incompatible_lock = tmp_path / "requirements-ci.txt" + incompatible_lock.write_text( + lock_text.replace(reviewed, "f" * 64, 1), + encoding="utf-8", + ) + + with pytest.raises(SystemExit, match="does not match"): + generator.validate_runtime_lock(MANIFEST_PATH, incompatible_lock) + PY + + set +e + python -m pytest -q tests/test_release_lock_hash_sets.py \ + >"${RUNNER_TEMP}/red.log" 2>&1 + status=$? + set -e + cat "${RUNNER_TEMP}/red.log" + test "$status" -ne 0 + grep -Eq 'exactly one SHA-256 hash|accepts_distinct_platform_hashes' "${RUNNER_TEMP}/red.log" + + - name: Apply minimum GREEN parser and changelog repair + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + script_path = Path("scripts/ci/generate_release_sbom.py") + script = script_path.read_text(encoding="utf-8") + replacements = [ + ( + "def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n", + "def _load_runtime_lock(\n" + " path: Path,\n" + ") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n", + ), + ( + ' """Load exact package versions, markers, and hashes from the CI lock."""\n', + ' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n', + ), + ( + " entries: dict[str, dict[str, str | None]] = {}\n", + " entries: dict[\n" + " str, dict[str, str | tuple[str, ...] | None]\n" + " ] = {}\n", + ), + ( + ''' if line.count("--hash=sha256:") != 1: + raise SystemExit("runtime lock entries require exactly one SHA-256 hash") + requirement_text, digest = line.split("--hash=sha256:", 1) + digest = digest.strip() + if SHA256.fullmatch(digest) is None: + raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + ''', + ''' segments = line.split("--hash=sha256:") + if len(segments) < 2: + raise SystemExit( + "runtime lock entries require at least one SHA-256 hash" + ) + requirement_text = segments[0] + digests = tuple(segment.strip() for segment in segments[1:]) + if any(SHA256.fullmatch(digest) is None for digest in digests): + raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + if len(set(digests)) != len(digests): + raise SystemExit("runtime lock contains a duplicate SHA-256 hash") + ''', + ), + (' "sha256": digest,\n', ' "sha256": digests,\n'), + ( + ''' for name, component in components.items(): + locked = lock_entries.get(name) + expected = { + "version": component["version"], + "sha256": component["sha256"], + "marker": _canonical_marker(component["marker"], f"component {name}"), + } + if locked != expected: + raise SystemExit( + f"component {name!r} does not match the hash-locked runtime subset" + ) + ''', + ''' for name, component in components.items(): + locked = lock_entries.get(name) + expected_marker = _canonical_marker( + component["marker"], f"component {name}" + ) + if ( + locked is None + or locked["version"] != component["version"] + or locked["marker"] != expected_marker + or component["sha256"] not in locked["sha256"] + ): + raise SystemExit( + f"component {name!r} does not match the hash-locked runtime subset" + ) + ''', + ), + ] + for old, new in replacements: + if script.count(old) != 1: + raise SystemExit(f"runtime-lock repair boundary drifted: {old[:72]!r}") + script = script.replace(old, new, 1) + script_path.write_text(script, encoding="utf-8") + + changelog_path = Path("CHANGELOG.md") + changelog = changelog_path.read_text(encoding="utf-8") + anchor = "### Fixed\n" + entry = ( + "- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while " + "requiring every reviewed runtime SBOM digest to be present in that immutable set. " + "This preserves portable hash-locked tooling without weakening release evidence.\n" + ) + if entry not in changelog: + if changelog.count(anchor) != 1: + raise SystemExit("CHANGELOG insertion boundary drifted") + changelog = changelog.replace(anchor, anchor + entry, 1) + changelog_path.write_text(changelog, encoding="utf-8") + PY + + - name: Verify focused and complete GREEN + shell: bash + run: | + set -euo pipefail + python -m pytest -q tests/test_release_lock_hash_sets.py + ruff check . + coverage erase + coverage run --branch -m pytest -q + coverage report --show-missing + python -m compileall -q src tests scripts + git diff --check + + - name: Publish verified repair and remove temporary authorities + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + rm .github/workflows/repair-pr-224.yml + rm .github/workflows/repair-pr-226-final.yml + git add -A + git config user.name "CWL Verified Repair" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "fix(sbom): accept portable multi-hash lock entries" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git fetch origin "${TARGET_BRANCH}" + test "$(git rev-parse FETCH_HEAD)" = "$GITHUB_SHA" + git push origin "HEAD:refs/heads/${TARGET_BRANCH}" From 1c8489a38f0f6842bc330bf5cbd2f4cbe5296177 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 23:07:54 -0700 Subject: [PATCH 06/15] ci: execute portable multi-hash lock repair --- .github/workflows/repair-pr-226-multihash.yml | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 .github/workflows/repair-pr-226-multihash.yml diff --git a/.github/workflows/repair-pr-226-multihash.yml b/.github/workflows/repair-pr-226-multihash.yml new file mode 100644 index 00000000..91b28483 --- /dev/null +++ b/.github/workflows/repair-pr-226-multihash.yml @@ -0,0 +1,314 @@ +name: Repair PR 226 portable hash locks + +on: + push: + branches: + - repair/ruff-0.16.3-multihash-lock + paths: + - .github/workflows/repair-pr-226-multihash.yml + +permissions: + contents: write + +concurrency: + group: repair-pr-226-multihash-${{ github.ref }} + cancel-in-progress: false + +jobs: + repair: + if: >- + github.repository == 'ContextualWisdomLab/EgressWeave' && + github.ref == 'refs/heads/repair/ruff-0.16.3-multihash-lock' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + TARGET_BRANCH: repair/ruff-0.16.3-multihash-lock + steps: + - name: Check out exact repair trigger + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + fetch-depth: 2 + persist-credentials: false + + - name: Verify exact single-writer boundary + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test "$(git branch --show-current)" = "$TARGET_BRANCH" + test "$(git status --porcelain)" = "" + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + cache: pip + cache-dependency-path: requirements-ci.txt + + - name: Install immutable CI toolchain + run: python -m pip install --require-hashes -r requirements-ci.txt + + - name: Demonstrate the portable-lock false rejection + shell: bash + run: | + set -euo pipefail + cat > tests/_red_release_lock_hash_set.py <<'PY' + """RED contract for one pinned version with multiple artifact hashes.""" + + import importlib.util + from pathlib import Path + + + def _load_generator(): + root = Path(__file__).resolve().parents[1] + path = root / "scripts" / "ci" / "generate_release_sbom.py" + spec = importlib.util.spec_from_file_location("egressweave_red_multihash", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + + def test_runtime_lock_accepts_two_distinct_artifact_hashes(tmp_path): + generator = _load_generator() + first = "a" * 64 + second = "b" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{first} --hash=sha256:{second}\n", + encoding="utf-8", + ) + + assert generator._load_runtime_lock(lock_path)["tool"]["sha256"] == ( + first, + second, + ) + PY + set +e + python -m pytest -q tests/_red_release_lock_hash_set.py \ + >"${RUNNER_TEMP}/multihash-red.log" 2>&1 + status=$? + set -e + cat "${RUNNER_TEMP}/multihash-red.log" + test "$status" -ne 0 + grep -F "exactly one SHA-256 hash" "${RUNNER_TEMP}/multihash-red.log" + rm tests/_red_release_lock_hash_set.py + + - name: Preserve exact portable artifact hash sets + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + from textwrap import dedent + + script_path = Path("scripts/ci/generate_release_sbom.py") + script = script_path.read_text(encoding="utf-8") + + replacements = [ + ( + "def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n", + "def _load_runtime_lock(\n" + " path: Path,\n" + ") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n", + ), + ( + ' """Load exact package versions, markers, and hashes from the CI lock."""\n', + ' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n', + ), + ( + " entries: dict[str, dict[str, str | None]] = {}\n", + " entries: dict[\n" + " str, dict[str, str | tuple[str, ...] | None]\n" + " ] = {}\n", + ), + ( + ''' if line.count("--hash=sha256:") != 1: + raise SystemExit("runtime lock entries require exactly one SHA-256 hash") + requirement_text, digest = line.split("--hash=sha256:", 1) + digest = digest.strip() + if SHA256.fullmatch(digest) is None: + raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + ''', + ''' segments = line.split("--hash=sha256:") + if len(segments) < 2: + raise SystemExit( + "runtime lock entries require at least one SHA-256 hash" + ) + requirement_text = segments[0] + digests = tuple(segment.strip() for segment in segments[1:]) + if any(SHA256.fullmatch(digest) is None for digest in digests): + raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + if len(set(digests)) != len(digests): + raise SystemExit("runtime lock contains a duplicate SHA-256 hash") + ''', + ), + (' "sha256": digest,\n', ' "sha256": digests,\n'), + ( + ''' for name, component in components.items(): + locked = lock_entries.get(name) + expected = { + "version": component["version"], + "sha256": component["sha256"], + "marker": _canonical_marker(component["marker"], f"component {name}"), + } + if locked != expected: + raise SystemExit( + f"component {name!r} does not match the hash-locked runtime subset" + ) + ''', + ''' for name, component in components.items(): + locked = lock_entries.get(name) + expected_marker = _canonical_marker( + component["marker"], f"component {name}" + ) + if ( + locked is None + or locked["version"] != component["version"] + or locked["marker"] != expected_marker + or component["sha256"] not in locked["sha256"] + ): + raise SystemExit( + f"component {name!r} does not match the hash-locked runtime subset" + ) + ''', + ), + ] + for old, new in replacements: + if script.count(old) != 1: + raise SystemExit(f"runtime-lock repair boundary drifted: {old[:72]!r}") + script = script.replace(old, new, 1) + script_path.write_text(script, encoding="utf-8") + + Path("tests/test_release_lock_hash_sets.py").write_text( + dedent( + '''\ + """Regression contracts for portable multi-artifact hash locks.""" + + from __future__ import annotations + + import importlib.util + from pathlib import Path + + import pytest + + REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" + MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" + LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" + + + def _load_generator(): + """Load the repository SBOM generator without packaging it.""" + specification = importlib.util.spec_from_file_location( + "egressweave_release_lock_hash_sets", + GENERATOR_PATH, + ) + assert specification is not None and specification.loader is not None + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module + + + def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: + """Preserve every exact artifact hash attached to one pinned version.""" + generator = _load_generator() + first_digest = "a" * 64 + second_digest = "b" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + "tool==1.2.3 " + f"--hash=sha256:{first_digest} " + f"--hash=sha256:{second_digest}\\n", + encoding="utf-8", + ) + + assert generator._load_runtime_lock(lock_path) == { + "tool": { + "version": "1.2.3", + "sha256": (first_digest, second_digest), + "marker": None, + } + } + + + def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: + """Reject ambiguous duplicate evidence within one package entry.""" + generator = _load_generator() + digest = "a" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n", + encoding="utf-8", + ) + + with pytest.raises(SystemExit, match="duplicate SHA-256"): + generator._load_runtime_lock(lock_path) + + + def test_runtime_manifest_accepts_its_digest_among_platform_hashes( + tmp_path: Path, + ) -> None: + """Bind runtime evidence to one reviewed digest in a portable hash set.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + needle = ( + "anyio==4.14.2 \\\\n" + " --hash=sha256:" + "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" + ) + replacement = needle + " \\\\n --hash=sha256:" + ("f" * 64) + assert lock_text.count(needle) == 1 + portable_lock = tmp_path / "requirements-ci.txt" + portable_lock.write_text( + lock_text.replace(needle, replacement, 1), + encoding="utf-8", + ) + + generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) + ''' + ), + encoding="utf-8", + ) + + changelog_path = Path("CHANGELOG.md") + changelog = changelog_path.read_text(encoding="utf-8") + anchor = "### Fixed\n" + addition = ( + "- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while " + "requiring every reviewed runtime SBOM digest to be present in that immutable set. " + "This preserves portable hash-locked tooling without weakening release evidence.\n" + ) + if changelog.count(anchor) != 1 or addition in changelog: + raise SystemExit("changelog insertion boundary drifted") + changelog_path.write_text( + changelog.replace(anchor, anchor + addition, 1), + encoding="utf-8", + ) + PY + + - name: Verify focused and complete GREEN + shell: bash + run: | + set -euo pipefail + ruff check . + coverage erase + coverage run -m pytest -q + coverage report -m + python -m compileall -q src tests scripts + git diff --check + + - name: Publish verified repair and remove temporary authority + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + rm -f .github/workflows/repair-pr-224.yml + rm .github/workflows/repair-pr-226-multihash.yml + git add -A + git config user.name "CWL One-shot Repair" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "fix(sbom): accept portable multi-hash lock entries" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git push origin "HEAD:refs/heads/${TARGET_BRANCH}" From 6fda581d24e4e66e0feb709f942030a59ab69147 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:31:39 +0900 Subject: [PATCH 07/15] chore(ci): remove purpose-complete repair workflow --- .github/workflows/repair-pr-224.yml | 261 ---------------------------- 1 file changed, 261 deletions(-) delete mode 100644 .github/workflows/repair-pr-224.yml diff --git a/.github/workflows/repair-pr-224.yml b/.github/workflows/repair-pr-224.yml deleted file mode 100644 index 34c51dae..00000000 --- a/.github/workflows/repair-pr-224.yml +++ /dev/null @@ -1,261 +0,0 @@ -name: Repair PR 224 multi-hash lock evidence - -on: - push: - branches: - - repair/ruff-0.16.3-multihash-lock - paths: - - .github/workflows/repair-pr-224.yml - -permissions: - contents: write - -concurrency: - group: repair-pr-224-${{ github.ref }} - cancel-in-progress: false - -jobs: - repair: - if: >- - github.repository == 'ContextualWisdomLab/EgressWeave' && - github.ref == 'refs/heads/repair/ruff-0.16.3-multihash-lock' - runs-on: ubuntu-24.04 - timeout-minutes: 35 - env: - EXPECTED_PARENT_SHA: 6ebfe145363229f06ee34898c75bd9ede2c6c6ac - TARGET_BRANCH: repair/ruff-0.16.3-multihash-lock - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - persist-credentials: false - - name: Verify exact repair boundary - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT_SHA" - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git branch --show-current)" = "$TARGET_BRANCH" - - name: Apply minimal multi-hash parser correction and regressions - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - from textwrap import dedent - - script_path = Path("scripts/ci/generate_release_sbom.py") - script = script_path.read_text(encoding="utf-8") - - replacements = [ - ( - "def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n", - "def _load_runtime_lock(\n" - " path: Path,\n" - ") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n", - ), - ( - ' """Load exact package versions, markers, and hashes from the CI lock."""\n', - ' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n', - ), - ( - " entries: dict[str, dict[str, str | None]] = {}\n", - " entries: dict[\n" - " str, dict[str, str | tuple[str, ...] | None]\n" - " ] = {}\n", - ), - ( - ''' if line.count("--hash=sha256:") != 1: - raise SystemExit("runtime lock entries require exactly one SHA-256 hash") - requirement_text, digest = line.split("--hash=sha256:", 1) - digest = digest.strip() - if SHA256.fullmatch(digest) is None: - raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - ''', - ''' segments = line.split("--hash=sha256:") - if len(segments) < 2: - raise SystemExit( - "runtime lock entries require at least one SHA-256 hash" - ) - requirement_text = segments[0] - digests = tuple(segment.strip() for segment in segments[1:]) - if any(SHA256.fullmatch(digest) is None for digest in digests): - raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - if len(set(digests)) != len(digests): - raise SystemExit("runtime lock contains a duplicate SHA-256 hash") - ''', - ), - (' "sha256": digest,\n', ' "sha256": digests,\n'), - ( - ''' for name, component in components.items(): - locked = lock_entries.get(name) - expected = { - "version": component["version"], - "sha256": component["sha256"], - "marker": _canonical_marker(component["marker"], f"component {name}"), - } - if locked != expected: - raise SystemExit( - f"component {name!r} does not match the hash-locked runtime subset" - ) - ''', - ''' for name, component in components.items(): - locked = lock_entries.get(name) - expected_marker = _canonical_marker( - component["marker"], f"component {name}" - ) - if ( - locked is None - or locked["version"] != component["version"] - or locked["marker"] != expected_marker - or component["sha256"] not in locked["sha256"] - ): - raise SystemExit( - f"component {name!r} does not match the hash-locked runtime subset" - ) - ''', - ), - ] - for old, new in replacements: - if script.count(old) != 1: - raise SystemExit(f"runtime-lock repair boundary drifted: {old[:60]!r}") - script = script.replace(old, new, 1) - script_path.write_text(script, encoding="utf-8") - - Path("tests/test_release_lock_hash_sets.py").write_text( - dedent( - '''\ - """Regression contracts for portable multi-artifact hash locks.""" - - from __future__ import annotations - - import importlib.util - from pathlib import Path - - import pytest - - REPOSITORY_ROOT = Path(__file__).resolve().parents[1] - GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" - MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" - LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" - - - def _load_generator(): - """Load the repository SBOM generator without packaging it.""" - specification = importlib.util.spec_from_file_location( - "egressweave_release_lock_hash_sets", - GENERATOR_PATH, - ) - assert specification is not None and specification.loader is not None - module = importlib.util.module_from_spec(specification) - specification.loader.exec_module(module) - return module - - - def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: - """Preserve every exact artifact hash attached to one pinned version.""" - generator = _load_generator() - first_digest = "a" * 64 - second_digest = "b" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - "tool==1.2.3 " - f"--hash=sha256:{first_digest} " - f"--hash=sha256:{second_digest}\\n", - encoding="utf-8", - ) - - assert generator._load_runtime_lock(lock_path) == { - "tool": { - "version": "1.2.3", - "sha256": (first_digest, second_digest), - "marker": None, - } - } - - - def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: - """Reject ambiguous duplicate evidence within one package entry.""" - generator = _load_generator() - digest = "a" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="duplicate SHA-256"): - generator._load_runtime_lock(lock_path) - - - def test_runtime_manifest_accepts_its_digest_among_platform_hashes( - tmp_path: Path, - ) -> None: - """Bind runtime evidence to one reviewed digest in a portable hash set.""" - generator = _load_generator() - lock_text = LOCK_PATH.read_text(encoding="utf-8") - needle = ( - "anyio==4.14.2 \\\n" - " --hash=sha256:" - "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" - ) - replacement = needle + " \\\n --hash=sha256:" + ("f" * 64) - assert lock_text.count(needle) == 1 - portable_lock = tmp_path / "requirements-ci.txt" - portable_lock.write_text( - lock_text.replace(needle, replacement, 1), - encoding="utf-8", - ) - - generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) - ''' - ), - encoding="utf-8", - ) - - changelog_path = Path("CHANGELOG.md") - changelog = changelog_path.read_text(encoding="utf-8") - anchor = "### Fixed\n" - addition = ( - "- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while " - "requiring every reviewed runtime SBOM digest to be present in that immutable set. " - "This preserves portable hash-locked tooling without weakening release evidence.\n" - ) - if changelog.count(anchor) != 1 or addition in changelog: - raise SystemExit("changelog insertion boundary drifted") - changelog_path.write_text( - changelog.replace(anchor, anchor + addition, 1), - encoding="utf-8", - ) - PY - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.14' - cache: pip - cache-dependency-path: requirements-ci.txt - - name: Install immutable CI toolchain - run: python -m pip install --require-hashes -r requirements-ci.txt - - name: Verify focused and complete GREEN - shell: bash - run: | - set -euo pipefail - ruff check . - coverage erase - coverage run -m pytest -q - coverage report -m - python -m compileall -q src tests scripts - git diff --check - - name: Publish verified repair and remove temporary authority - env: - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - rm .github/workflows/repair-pr-224.yml - git add -A - git config user.name "CWL One-shot Repair" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "fix(sbom): accept portable multi-hash lock entries" - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - git push origin "HEAD:refs/heads/${TARGET_BRANCH}" From 9f44c08212090eef5cdbaa48666d8f8de2e2dedd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:31:45 +0900 Subject: [PATCH 08/15] chore(ci): remove purpose-complete repair workflow --- .github/workflows/repair-pr-226-final.yml | 312 ---------------------- 1 file changed, 312 deletions(-) delete mode 100644 .github/workflows/repair-pr-226-final.yml diff --git a/.github/workflows/repair-pr-226-final.yml b/.github/workflows/repair-pr-226-final.yml deleted file mode 100644 index d526b1ec..00000000 --- a/.github/workflows/repair-pr-226-final.yml +++ /dev/null @@ -1,312 +0,0 @@ -name: Repair PR 226 portable hash-lock evidence - -on: - push: - branches: - - repair/ruff-0.16.3-multihash-lock - paths: - - .github/workflows/repair-pr-226-final.yml - -permissions: - contents: read - -concurrency: - group: repair-pr-226-final-${{ github.ref }} - cancel-in-progress: false - -jobs: - repair: - if: >- - github.repository == 'ContextualWisdomLab/EgressWeave' && - github.ref == 'refs/heads/repair/ruff-0.16.3-multihash-lock' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: write - env: - EXPECTED_PARENT_SHA: 52f1885d45f3126e2f7abbd9558a34cc1bfb3a39 - TARGET_BRANCH: repair/ruff-0.16.3-multihash-lock - steps: - - name: Check out exact repair trigger - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - persist-credentials: false - - - name: Verify exact single-writer boundary - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT_SHA" - test "$(git status --porcelain)" = "" - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.14' - cache: pip - cache-dependency-path: requirements-ci.txt - - - name: Install immutable CI toolchain - run: python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-ci.txt - - - name: Add RED portable-lock regressions - shell: bash - run: | - set -euo pipefail - cat > tests/test_release_lock_hash_sets.py <<'PY' - """Regression contracts for portable multi-artifact hash locks.""" - - from __future__ import annotations - - import importlib.util - from pathlib import Path - - import pytest - - REPOSITORY_ROOT = Path(__file__).resolve().parents[1] - GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" - MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" - LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" - - - def _load_generator(): - """Load the repository SBOM generator without packaging it.""" - specification = importlib.util.spec_from_file_location( - "egressweave_release_lock_hash_sets", - GENERATOR_PATH, - ) - assert specification is not None and specification.loader is not None - module = importlib.util.module_from_spec(specification) - specification.loader.exec_module(module) - return module - - - def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: - """Preserve every exact artifact hash attached to one pinned version.""" - generator = _load_generator() - first_digest = "a" * 64 - second_digest = "b" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - "tool==1.2.3 " - f"--hash=sha256:{first_digest} " - f"--hash=sha256:{second_digest}\n", - encoding="utf-8", - ) - - assert generator._load_runtime_lock(lock_path) == { - "tool": { - "version": "1.2.3", - "sha256": (first_digest, second_digest), - "marker": None, - } - } - - - def test_runtime_lock_keeps_one_hash_as_a_hash_set(tmp_path: Path) -> None: - """Represent a one-artifact lock with the same immutable tuple contract.""" - generator = _load_generator() - digest = "a" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - f"tool==1.2.3 --hash=sha256:{digest}\n", - encoding="utf-8", - ) - - assert generator._load_runtime_lock(lock_path)["tool"]["sha256"] == ( - digest, - ) - - - def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: - """Reject ambiguous duplicate evidence within one package entry.""" - generator = _load_generator() - digest = "a" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="duplicate SHA-256"): - generator._load_runtime_lock(lock_path) - - - def test_runtime_manifest_accepts_its_digest_among_platform_hashes( - tmp_path: Path, - ) -> None: - """Bind runtime evidence to one reviewed digest in a portable hash set.""" - generator = _load_generator() - lock_text = LOCK_PATH.read_text(encoding="utf-8") - needle = ( - "anyio==4.14.2 \\\n" - " --hash=sha256:" - "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" - ) - replacement = needle + " \\\n --hash=sha256:" + ("f" * 64) - assert lock_text.count(needle) == 1 - portable_lock = tmp_path / "requirements-ci.txt" - portable_lock.write_text( - lock_text.replace(needle, replacement, 1), - encoding="utf-8", - ) - - generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) - - - def test_runtime_manifest_rejects_digest_outside_reviewed_hash_set( - tmp_path: Path, - ) -> None: - """Reject a runtime artifact digest absent from its immutable lock set.""" - generator = _load_generator() - lock_text = LOCK_PATH.read_text(encoding="utf-8") - reviewed = "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" - assert lock_text.count(reviewed) == 1 - incompatible_lock = tmp_path / "requirements-ci.txt" - incompatible_lock.write_text( - lock_text.replace(reviewed, "f" * 64, 1), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="does not match"): - generator.validate_runtime_lock(MANIFEST_PATH, incompatible_lock) - PY - - set +e - python -m pytest -q tests/test_release_lock_hash_sets.py \ - >"${RUNNER_TEMP}/red.log" 2>&1 - status=$? - set -e - cat "${RUNNER_TEMP}/red.log" - test "$status" -ne 0 - grep -Eq 'exactly one SHA-256 hash|accepts_distinct_platform_hashes' "${RUNNER_TEMP}/red.log" - - - name: Apply minimum GREEN parser and changelog repair - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - script_path = Path("scripts/ci/generate_release_sbom.py") - script = script_path.read_text(encoding="utf-8") - replacements = [ - ( - "def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n", - "def _load_runtime_lock(\n" - " path: Path,\n" - ") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n", - ), - ( - ' """Load exact package versions, markers, and hashes from the CI lock."""\n', - ' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n', - ), - ( - " entries: dict[str, dict[str, str | None]] = {}\n", - " entries: dict[\n" - " str, dict[str, str | tuple[str, ...] | None]\n" - " ] = {}\n", - ), - ( - ''' if line.count("--hash=sha256:") != 1: - raise SystemExit("runtime lock entries require exactly one SHA-256 hash") - requirement_text, digest = line.split("--hash=sha256:", 1) - digest = digest.strip() - if SHA256.fullmatch(digest) is None: - raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - ''', - ''' segments = line.split("--hash=sha256:") - if len(segments) < 2: - raise SystemExit( - "runtime lock entries require at least one SHA-256 hash" - ) - requirement_text = segments[0] - digests = tuple(segment.strip() for segment in segments[1:]) - if any(SHA256.fullmatch(digest) is None for digest in digests): - raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - if len(set(digests)) != len(digests): - raise SystemExit("runtime lock contains a duplicate SHA-256 hash") - ''', - ), - (' "sha256": digest,\n', ' "sha256": digests,\n'), - ( - ''' for name, component in components.items(): - locked = lock_entries.get(name) - expected = { - "version": component["version"], - "sha256": component["sha256"], - "marker": _canonical_marker(component["marker"], f"component {name}"), - } - if locked != expected: - raise SystemExit( - f"component {name!r} does not match the hash-locked runtime subset" - ) - ''', - ''' for name, component in components.items(): - locked = lock_entries.get(name) - expected_marker = _canonical_marker( - component["marker"], f"component {name}" - ) - if ( - locked is None - or locked["version"] != component["version"] - or locked["marker"] != expected_marker - or component["sha256"] not in locked["sha256"] - ): - raise SystemExit( - f"component {name!r} does not match the hash-locked runtime subset" - ) - ''', - ), - ] - for old, new in replacements: - if script.count(old) != 1: - raise SystemExit(f"runtime-lock repair boundary drifted: {old[:72]!r}") - script = script.replace(old, new, 1) - script_path.write_text(script, encoding="utf-8") - - changelog_path = Path("CHANGELOG.md") - changelog = changelog_path.read_text(encoding="utf-8") - anchor = "### Fixed\n" - entry = ( - "- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while " - "requiring every reviewed runtime SBOM digest to be present in that immutable set. " - "This preserves portable hash-locked tooling without weakening release evidence.\n" - ) - if entry not in changelog: - if changelog.count(anchor) != 1: - raise SystemExit("CHANGELOG insertion boundary drifted") - changelog = changelog.replace(anchor, anchor + entry, 1) - changelog_path.write_text(changelog, encoding="utf-8") - PY - - - name: Verify focused and complete GREEN - shell: bash - run: | - set -euo pipefail - python -m pytest -q tests/test_release_lock_hash_sets.py - ruff check . - coverage erase - coverage run --branch -m pytest -q - coverage report --show-missing - python -m compileall -q src tests scripts - git diff --check - - - name: Publish verified repair and remove temporary authorities - env: - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - rm .github/workflows/repair-pr-224.yml - rm .github/workflows/repair-pr-226-final.yml - git add -A - git config user.name "CWL Verified Repair" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "fix(sbom): accept portable multi-hash lock entries" - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - git fetch origin "${TARGET_BRANCH}" - test "$(git rev-parse FETCH_HEAD)" = "$GITHUB_SHA" - git push origin "HEAD:refs/heads/${TARGET_BRANCH}" From f7e426eb735fbc683f7c4b1097cbbeefda5fcbe3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:31:52 +0900 Subject: [PATCH 09/15] chore(ci): remove purpose-complete repair workflow --- .github/workflows/repair-pr-226-multihash.yml | 314 ------------------ 1 file changed, 314 deletions(-) delete mode 100644 .github/workflows/repair-pr-226-multihash.yml diff --git a/.github/workflows/repair-pr-226-multihash.yml b/.github/workflows/repair-pr-226-multihash.yml deleted file mode 100644 index 91b28483..00000000 --- a/.github/workflows/repair-pr-226-multihash.yml +++ /dev/null @@ -1,314 +0,0 @@ -name: Repair PR 226 portable hash locks - -on: - push: - branches: - - repair/ruff-0.16.3-multihash-lock - paths: - - .github/workflows/repair-pr-226-multihash.yml - -permissions: - contents: write - -concurrency: - group: repair-pr-226-multihash-${{ github.ref }} - cancel-in-progress: false - -jobs: - repair: - if: >- - github.repository == 'ContextualWisdomLab/EgressWeave' && - github.ref == 'refs/heads/repair/ruff-0.16.3-multihash-lock' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - env: - TARGET_BRANCH: repair/ruff-0.16.3-multihash-lock - steps: - - name: Check out exact repair trigger - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - persist-credentials: false - - - name: Verify exact single-writer boundary - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git branch --show-current)" = "$TARGET_BRANCH" - test "$(git status --porcelain)" = "" - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.14' - cache: pip - cache-dependency-path: requirements-ci.txt - - - name: Install immutable CI toolchain - run: python -m pip install --require-hashes -r requirements-ci.txt - - - name: Demonstrate the portable-lock false rejection - shell: bash - run: | - set -euo pipefail - cat > tests/_red_release_lock_hash_set.py <<'PY' - """RED contract for one pinned version with multiple artifact hashes.""" - - import importlib.util - from pathlib import Path - - - def _load_generator(): - root = Path(__file__).resolve().parents[1] - path = root / "scripts" / "ci" / "generate_release_sbom.py" - spec = importlib.util.spec_from_file_location("egressweave_red_multihash", path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - - def test_runtime_lock_accepts_two_distinct_artifact_hashes(tmp_path): - generator = _load_generator() - first = "a" * 64 - second = "b" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - f"tool==1.2.3 --hash=sha256:{first} --hash=sha256:{second}\n", - encoding="utf-8", - ) - - assert generator._load_runtime_lock(lock_path)["tool"]["sha256"] == ( - first, - second, - ) - PY - set +e - python -m pytest -q tests/_red_release_lock_hash_set.py \ - >"${RUNNER_TEMP}/multihash-red.log" 2>&1 - status=$? - set -e - cat "${RUNNER_TEMP}/multihash-red.log" - test "$status" -ne 0 - grep -F "exactly one SHA-256 hash" "${RUNNER_TEMP}/multihash-red.log" - rm tests/_red_release_lock_hash_set.py - - - name: Preserve exact portable artifact hash sets - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - from textwrap import dedent - - script_path = Path("scripts/ci/generate_release_sbom.py") - script = script_path.read_text(encoding="utf-8") - - replacements = [ - ( - "def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]:\n", - "def _load_runtime_lock(\n" - " path: Path,\n" - ") -> dict[str, dict[str, str | tuple[str, ...] | None]]:\n", - ), - ( - ' """Load exact package versions, markers, and hashes from the CI lock."""\n', - ' """Load exact package versions, markers, and allowed hashes from the CI lock."""\n', - ), - ( - " entries: dict[str, dict[str, str | None]] = {}\n", - " entries: dict[\n" - " str, dict[str, str | tuple[str, ...] | None]\n" - " ] = {}\n", - ), - ( - ''' if line.count("--hash=sha256:") != 1: - raise SystemExit("runtime lock entries require exactly one SHA-256 hash") - requirement_text, digest = line.split("--hash=sha256:", 1) - digest = digest.strip() - if SHA256.fullmatch(digest) is None: - raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - ''', - ''' segments = line.split("--hash=sha256:") - if len(segments) < 2: - raise SystemExit( - "runtime lock entries require at least one SHA-256 hash" - ) - requirement_text = segments[0] - digests = tuple(segment.strip() for segment in segments[1:]) - if any(SHA256.fullmatch(digest) is None for digest in digests): - raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") - if len(set(digests)) != len(digests): - raise SystemExit("runtime lock contains a duplicate SHA-256 hash") - ''', - ), - (' "sha256": digest,\n', ' "sha256": digests,\n'), - ( - ''' for name, component in components.items(): - locked = lock_entries.get(name) - expected = { - "version": component["version"], - "sha256": component["sha256"], - "marker": _canonical_marker(component["marker"], f"component {name}"), - } - if locked != expected: - raise SystemExit( - f"component {name!r} does not match the hash-locked runtime subset" - ) - ''', - ''' for name, component in components.items(): - locked = lock_entries.get(name) - expected_marker = _canonical_marker( - component["marker"], f"component {name}" - ) - if ( - locked is None - or locked["version"] != component["version"] - or locked["marker"] != expected_marker - or component["sha256"] not in locked["sha256"] - ): - raise SystemExit( - f"component {name!r} does not match the hash-locked runtime subset" - ) - ''', - ), - ] - for old, new in replacements: - if script.count(old) != 1: - raise SystemExit(f"runtime-lock repair boundary drifted: {old[:72]!r}") - script = script.replace(old, new, 1) - script_path.write_text(script, encoding="utf-8") - - Path("tests/test_release_lock_hash_sets.py").write_text( - dedent( - '''\ - """Regression contracts for portable multi-artifact hash locks.""" - - from __future__ import annotations - - import importlib.util - from pathlib import Path - - import pytest - - REPOSITORY_ROOT = Path(__file__).resolve().parents[1] - GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" - MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" - LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" - - - def _load_generator(): - """Load the repository SBOM generator without packaging it.""" - specification = importlib.util.spec_from_file_location( - "egressweave_release_lock_hash_sets", - GENERATOR_PATH, - ) - assert specification is not None and specification.loader is not None - module = importlib.util.module_from_spec(specification) - specification.loader.exec_module(module) - return module - - - def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: - """Preserve every exact artifact hash attached to one pinned version.""" - generator = _load_generator() - first_digest = "a" * 64 - second_digest = "b" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - "tool==1.2.3 " - f"--hash=sha256:{first_digest} " - f"--hash=sha256:{second_digest}\\n", - encoding="utf-8", - ) - - assert generator._load_runtime_lock(lock_path) == { - "tool": { - "version": "1.2.3", - "sha256": (first_digest, second_digest), - "marker": None, - } - } - - - def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: - """Reject ambiguous duplicate evidence within one package entry.""" - generator = _load_generator() - digest = "a" * 64 - lock_path = tmp_path / "requirements-ci.txt" - lock_path.write_text( - f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="duplicate SHA-256"): - generator._load_runtime_lock(lock_path) - - - def test_runtime_manifest_accepts_its_digest_among_platform_hashes( - tmp_path: Path, - ) -> None: - """Bind runtime evidence to one reviewed digest in a portable hash set.""" - generator = _load_generator() - lock_text = LOCK_PATH.read_text(encoding="utf-8") - needle = ( - "anyio==4.14.2 \\\\n" - " --hash=sha256:" - "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" - ) - replacement = needle + " \\\\n --hash=sha256:" + ("f" * 64) - assert lock_text.count(needle) == 1 - portable_lock = tmp_path / "requirements-ci.txt" - portable_lock.write_text( - lock_text.replace(needle, replacement, 1), - encoding="utf-8", - ) - - generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) - ''' - ), - encoding="utf-8", - ) - - changelog_path = Path("CHANGELOG.md") - changelog = changelog_path.read_text(encoding="utf-8") - anchor = "### Fixed\n" - addition = ( - "- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while " - "requiring every reviewed runtime SBOM digest to be present in that immutable set. " - "This preserves portable hash-locked tooling without weakening release evidence.\n" - ) - if changelog.count(anchor) != 1 or addition in changelog: - raise SystemExit("changelog insertion boundary drifted") - changelog_path.write_text( - changelog.replace(anchor, anchor + addition, 1), - encoding="utf-8", - ) - PY - - - name: Verify focused and complete GREEN - shell: bash - run: | - set -euo pipefail - ruff check . - coverage erase - coverage run -m pytest -q - coverage report -m - python -m compileall -q src tests scripts - git diff --check - - - name: Publish verified repair and remove temporary authority - env: - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - rm -f .github/workflows/repair-pr-224.yml - rm .github/workflows/repair-pr-226-multihash.yml - git add -A - git config user.name "CWL One-shot Repair" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "fix(sbom): accept portable multi-hash lock entries" - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - git push origin "HEAD:refs/heads/${TARGET_BRANCH}" From e7d1d13d779a2b4ef7956d40a6b646355d49c3f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:36:03 +0900 Subject: [PATCH 10/15] test(sbom): cover portable multi-hash lock evidence --- tests/test_release_lock_hash_sets.py | 83 ++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_release_lock_hash_sets.py diff --git a/tests/test_release_lock_hash_sets.py b/tests/test_release_lock_hash_sets.py new file mode 100644 index 00000000..c04b8fdb --- /dev/null +++ b/tests/test_release_lock_hash_sets.py @@ -0,0 +1,83 @@ +"""Regression contracts for portable multi-artifact hash locks.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "generate_release_sbom.py" +MANIFEST_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "release_runtime_dependencies.json" +LOCK_PATH = REPOSITORY_ROOT / "requirements-ci.txt" + + +def _load_generator(): + """Load the repository SBOM generator without packaging it.""" + specification = importlib.util.spec_from_file_location( + "egressweave_release_lock_hash_sets", + GENERATOR_PATH, + ) + assert specification is not None and specification.loader is not None + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module + + +def test_runtime_lock_accepts_distinct_platform_hashes(tmp_path: Path) -> None: + """Preserve every exact artifact hash attached to one pinned version.""" + generator = _load_generator() + first_digest = "a" * 64 + second_digest = "b" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + "tool==1.2.3 " + f"--hash=sha256:{first_digest} " + f"--hash=sha256:{second_digest}\n", + encoding="utf-8", + ) + + assert generator._load_runtime_lock(lock_path) == { + "tool": { + "version": "1.2.3", + "sha256": (first_digest, second_digest), + "marker": None, + } + } + + +def test_runtime_lock_rejects_duplicate_artifact_hashes(tmp_path: Path) -> None: + """Reject duplicate evidence within one package hash set.""" + generator = _load_generator() + digest = "a" * 64 + lock_path = tmp_path / "requirements-ci.txt" + lock_path.write_text( + f"tool==1.2.3 --hash=sha256:{digest} --hash=sha256:{digest}\n", + encoding="utf-8", + ) + + with pytest.raises(SystemExit, match="duplicate SHA-256"): + generator._load_runtime_lock(lock_path) + + +def test_runtime_manifest_accepts_its_digest_among_platform_hashes( + tmp_path: Path, +) -> None: + """Bind runtime evidence to one reviewed digest in a portable hash set.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + needle = ( + "anyio==4.14.2 \\\n" + " --hash=sha256:" + "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" + ) + replacement = needle + " \\\n --hash=sha256:" + ("f" * 64) + assert lock_text.count(needle) == 1 + portable_lock = tmp_path / "requirements-ci.txt" + portable_lock.write_text( + lock_text.replace(needle, replacement, 1), + encoding="utf-8", + ) + + generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) From f83c68facb90471a08b7fcff7fd8319029665bde Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:41:40 +0900 Subject: [PATCH 11/15] fix(sbom): accept portable multi-hash lock entries --- scripts/ci/generate_release_sbom.py | 43 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/scripts/ci/generate_release_sbom.py b/scripts/ci/generate_release_sbom.py index 23654c03..19b0104f 100644 --- a/scripts/ci/generate_release_sbom.py +++ b/scripts/ci/generate_release_sbom.py @@ -446,7 +446,6 @@ def _preflight_sdist_members_detailed(stream: BinaryIO) -> BinaryIO: consumed = 0 members = 0 zero_headers = 0 - # The validated snapshot lifetime is transferred to _sdist_metadata. expanded_archive = tempfile.TemporaryFile(mode="w+b") # noqa: SIM115 try: with gzip.GzipFile(fileobj=stream, mode="rb") as expanded: @@ -783,25 +782,32 @@ def _canonical_marker(value: str | None, context: str) -> str | None: raise SystemExit(f"{context} contains an invalid environment marker") from error -def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]: - """Load exact package versions, markers, and hashes from the CI lock.""" +def _load_runtime_lock( + path: Path, +) -> dict[str, dict[str, str | tuple[str, ...] | None]]: + """Load exact package versions, markers, and allowed hashes from the CI lock.""" try: if path.stat().st_size > MAX_MANIFEST_BYTES: raise SystemExit("runtime lock exceeds the safety bound") content = path.read_text(encoding="utf-8") except (OSError, UnicodeError) as error: raise SystemExit("runtime lock is unreadable") from error - entries: dict[str, dict[str, str | None]] = {} + entries: dict[str, dict[str, str | tuple[str, ...] | None]] = {} for raw_line in content.replace("\\\n", " ").splitlines(): line = raw_line.strip() if not line or line.startswith("#"): continue - if line.count("--hash=sha256:") != 1: - raise SystemExit("runtime lock entries require exactly one SHA-256 hash") - requirement_text, digest = line.split("--hash=sha256:", 1) - digest = digest.strip() - if SHA256.fullmatch(digest) is None: + segments = line.split("--hash=sha256:") + if len(segments) < 2: + raise SystemExit( + "runtime lock entries require at least one SHA-256 hash" + ) + requirement_text = segments[0] + digests = tuple(segment.strip() for segment in segments[1:]) + if any(SHA256.fullmatch(digest) is None for digest in digests): raise SystemExit("runtime lock contains a noncanonical SHA-256 hash") + if len(set(digests)) != len(digests): + raise SystemExit("runtime lock contains a duplicate SHA-256 hash") try: requirement = Requirement(requirement_text.strip()) except InvalidRequirement as error: @@ -821,24 +827,27 @@ def _load_runtime_lock(path: Path) -> dict[str, dict[str, str | None]]: ) entries[name] = { "version": version, - "sha256": digest, + "sha256": digests, "marker": str(requirement.marker) if requirement.marker is not None else None, } return entries def validate_runtime_lock(manifest_path: Path, lock_path: Path) -> None: - """Require every SBOM dependency to equal its executable lock evidence.""" + """Require every SBOM dependency to match one reviewed lock artifact.""" _, components = _load_manifest(manifest_path) lock_entries = _load_runtime_lock(lock_path) for name, component in components.items(): locked = lock_entries.get(name) - expected = { - "version": component["version"], - "sha256": component["sha256"], - "marker": _canonical_marker(component["marker"], f"component {name}"), - } - if locked != expected: + expected_marker = _canonical_marker( + component["marker"], f"component {name}" + ) + if ( + locked is None + or locked["version"] != component["version"] + or locked["marker"] != expected_marker + or component["sha256"] not in locked["sha256"] + ): raise SystemExit( f"component {name!r} does not match the hash-locked runtime subset" ) From 1a48accc693ffe35d96ec32ec17c5b756880d5c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:48:35 +0900 Subject: [PATCH 12/15] docs(changelog): record portable multi-hash lock repair --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfaecbc..484babe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ambiguous or non-positive configuration before network I/O. ### Fixed +- Accept distinct exact SHA-256 artifact hashes for one pinned CI package while + requiring every reviewed runtime SBOM digest to be present in that immutable + set, preserving portable hash-locked tooling without weakening release evidence. - Restore the hourly PR-maintenance calls to the reviewed immutable `ContextualWisdomLab/.github` revision `59505c1d89eb7ea816e921b6da38079c736608c2`. The regression contract now @@ -427,6 +430,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `build_egress_http_client` / `build_pinned_https_async_client` — DNS-pinned `httpx.AsyncClient` closing the validate-then-connect TOCTOU / DNS-rebinding gap (CWE-350), with redirects and environment proxies disabled. -- `EgressNotAllowedError` (a `ValueError` subclass) and `ValidatedEgressURL`. - 35 tests covering URL rejection, address classification, the `allow_local` container case, DNS-to-private rejection, and transport pinning. +- `build_egress_http_client` and `build_pinned_https_async_client` preserve exact + allowlist behavior and deny redirects/proxies under the public API. From 7037094fb258e631681557ef832ccbf46dce1f50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:54:22 +0900 Subject: [PATCH 13/15] chore(sbom): preserve snapshot lifetime rationale --- scripts/ci/generate_release_sbom.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci/generate_release_sbom.py b/scripts/ci/generate_release_sbom.py index 19b0104f..acc89ceb 100644 --- a/scripts/ci/generate_release_sbom.py +++ b/scripts/ci/generate_release_sbom.py @@ -446,6 +446,7 @@ def _preflight_sdist_members_detailed(stream: BinaryIO) -> BinaryIO: consumed = 0 members = 0 zero_headers = 0 + # The validated snapshot lifetime is transferred to _sdist_metadata. expanded_archive = tempfile.TemporaryFile(mode="w+b") # noqa: SIM115 try: with gzip.GzipFile(fileobj=stream, mode="rb") as expanded: From 9c97f91dc556f6060d2da40a3c5543b19d170290 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:55:45 +0900 Subject: [PATCH 14/15] docs(changelog): restore preserved release history --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484babe2..3199052d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -430,7 +430,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `build_egress_http_client` / `build_pinned_https_async_client` — DNS-pinned `httpx.AsyncClient` closing the validate-then-connect TOCTOU / DNS-rebinding gap (CWE-350), with redirects and environment proxies disabled. +- `EgressNotAllowedError` (a `ValueError` subclass) and `ValidatedEgressURL`. - 35 tests covering URL rejection, address classification, the `allow_local` container case, DNS-to-private rejection, and transport pinning. -- `build_egress_http_client` and `build_pinned_https_async_client` preserve exact - allowlist behavior and deny redirects/proxies under the public API. From b0a811990b181a5d640cd2cfd64824418573c241 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:59:14 +0900 Subject: [PATCH 15/15] test(sbom): reject absent digest in portable hash set --- tests/test_release_lock_hash_sets.py | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_release_lock_hash_sets.py b/tests/test_release_lock_hash_sets.py index c04b8fdb..6c4dce98 100644 --- a/tests/test_release_lock_hash_sets.py +++ b/tests/test_release_lock_hash_sets.py @@ -81,3 +81,32 @@ def test_runtime_manifest_accepts_its_digest_among_platform_hashes( ) generator.validate_runtime_lock(MANIFEST_PATH, portable_lock) + + +def test_runtime_manifest_rejects_absent_digest_from_platform_hashes( + tmp_path: Path, +) -> None: + """Reject a hash set that omits the exact reviewed runtime artifact digest.""" + generator = _load_generator() + lock_text = LOCK_PATH.read_text(encoding="utf-8") + needle = ( + "anyio==4.14.2 \\\n" + " --hash=sha256:" + "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" + ) + replacement = ( + "anyio==4.14.2 \\\n" + " --hash=sha256:" + + ("e" * 64) + + " \\\n --hash=sha256:" + + ("f" * 64) + ) + assert lock_text.count(needle) == 1 + portable_lock = tmp_path / "requirements-ci.txt" + portable_lock.write_text( + lock_text.replace(needle, replacement, 1), + encoding="utf-8", + ) + + with pytest.raises(SystemExit, match="does not match the hash-locked runtime subset"): + generator.validate_runtime_lock(MANIFEST_PATH, portable_lock)