From 9c02e28b5341b74044baff1ed70f19bf47cc8b58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 19:55:20 +0900 Subject: [PATCH 01/20] fix(coverage): materialize trusted uv lock dependencies --- .../materialize_base_python_requirements.py | 169 +++++++-- ...st_materialize_base_python_requirements.py | 331 +++++++++++++++++- 2 files changed, 461 insertions(+), 39 deletions(-) mode change 100644 => 100755 scripts/ci/materialize_base_python_requirements.py diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py old mode 100644 new mode 100755 index 8158372df..1a1dd87f7 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -4,18 +4,38 @@ from __future__ import annotations import argparse +import atexit import fnmatch +import functools +import hashlib +import io import json import pathlib import re import shutil import subprocess import sys +import tarfile import tempfile +import urllib.parse +import urllib.request SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") UV_EXPORT_TIMEOUT_SECONDS = 120 +TRUSTED_UV_VERSION = "0.12.1" +TRUSTED_UV_ARCHIVE_URL = ( + "https://releases.astral.sh/github/uv/releases/download/0.12.1/" + "uv-x86_64-unknown-linux-gnu.tar.gz" +) +TRUSTED_UV_ARCHIVE_SHA256 = ( + "90b2f223fb69d19db49e117da601f64978593417988530aa733d456141b4bcbb" +) +TRUSTED_UV_ARCHIVE_MEMBER = "uv-x86_64-unknown-linux-gnu/uv" +TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS = 120 +TRUSTED_UV_DOWNLOAD_MAX_BYTES = 64 * 1024 * 1024 +TRUSTED_UV_BINARY_MAX_BYTES = 64 * 1024 * 1024 +TRUSTED_UV_VERSION_TIMEOUT_SECONDS = 10 def _is_candidate_lock_name(name: str) -> bool: @@ -80,6 +100,98 @@ def _git(repo_root: pathlib.Path, *args: str) -> bytes: return completed.stdout +def _download_trusted_uv_archive() -> bytes: + """Download the fixed uv release archive through one HTTPS trust boundary.""" + request = urllib.request.Request( + TRUSTED_UV_ARCHIVE_URL, + headers={"User-Agent": "ContextualWisdomLab-coverage/1"}, + method="GET", + ) + try: + with urllib.request.urlopen( # nosec B310 -- fixed URL plus SHA-256 pin + request, timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS + ) as response: + final_url = urllib.parse.urlparse(response.geturl()) + if (final_url.scheme, final_url.hostname) != ( + "https", + "releases.astral.sh", + ): + raise RuntimeError( + "trusted uv archive redirected outside releases.astral.sh" + ) + payload = response.read(TRUSTED_UV_DOWNLOAD_MAX_BYTES + 1) + except OSError as exc: + raise RuntimeError( + f"trusted uv archive download failed: {type(exc).__name__}" + ) from exc + + if len(payload) > TRUSTED_UV_DOWNLOAD_MAX_BYTES: + raise RuntimeError("trusted uv archive exceeded the bounded download size") + return payload + + +def _verified_uv_binary(archive_payload: bytes) -> bytes: + """Return the bounded uv executable after archive and member verification.""" + digest = hashlib.sha256(archive_payload).hexdigest() + if digest != TRUSTED_UV_ARCHIVE_SHA256: + raise RuntimeError("trusted uv archive checksum verification failed") + + try: + with tarfile.open(fileobj=io.BytesIO(archive_payload), mode="r:gz") as bundle: + try: + member = bundle.getmember(TRUSTED_UV_ARCHIVE_MEMBER) + except KeyError as exc: + raise RuntimeError("trusted uv archive omitted the uv executable") from exc + if not member.isfile(): + raise RuntimeError("trusted uv archive member is not a regular file") + if member.size > TRUSTED_UV_BINARY_MAX_BYTES: + raise RuntimeError("trusted uv executable exceeded the bounded size") + extracted = bundle.extractfile(member) + if extracted is None: # pragma: no cover - guarded by member.isfile() + raise AssertionError("regular tar members must be extractable") + binary = extracted.read(TRUSTED_UV_BINARY_MAX_BYTES + 1) + except tarfile.TarError as exc: + raise RuntimeError("trusted uv archive could not be parsed") from exc + + if len(binary) != member.size: + raise RuntimeError("trusted uv executable size did not match its archive metadata") + return binary + + +@functools.cache +def _install_trusted_uv() -> str: + """Install and verify the pinned uv exporter once for this process.""" + tool_dir = pathlib.Path(tempfile.mkdtemp(prefix="opencode-trusted-uv-")) + tool_dir.mkdir(mode=0o700, parents=True, exist_ok=True) + uv_path = tool_dir / "uv" + try: + uv_path.write_bytes(_verified_uv_binary(_download_trusted_uv_archive())) + uv_path.chmod(0o755) + try: + completed = subprocess.run( + [str(uv_path), "--version"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=TRUSTED_UV_VERSION_TIMEOUT_SECONDS, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise RuntimeError( + f"trusted uv executable verification failed: {type(exc).__name__}" + ) from exc + observed = completed.stdout.decode("utf-8", errors="replace").strip() + if completed.returncode != 0 or observed != f"uv {TRUSTED_UV_VERSION}": + raise RuntimeError( + "trusted uv executable reported an unexpected version or exit status" + ) + except Exception: + shutil.rmtree(tool_dir, ignore_errors=True) + raise + + atexit.register(shutil.rmtree, tool_dir, ignore_errors=True) + return str(uv_path) + + def _run_uv_export( work_dir: pathlib.Path, uv_path: str, @@ -116,46 +228,61 @@ def _run_uv_export( def _export_uv_lock( repo_root: pathlib.Path, base_sha: str, lock_path: str ) -> bytes | None: - """Export a base ``uv.lock`` to a hash-pinned requirements closure, or ``None``. - - ``uv.lock`` is not a pip-installable format, so a uv-managed repository - materializes no dependencies and its offline coverage run fails at import. - When ``uv`` is available, reconstruct the exact base ``uv.lock`` and its - sibling ``pyproject.toml`` in an isolated temporary directory and run - ``uv export --frozen`` to produce a fully hash-pinned closure the trusted - installer can consume like any other lock. Both inputs are read only from - the validated base commit, so no PR-mutable content reaches ``uv``. Return - ``None`` — degrading to the prior no-uv behavior — when ``uv`` is absent, - the sibling ``pyproject.toml`` is missing at the base commit, the export - fails, or its output is not fully hash-pinned, so this can never break an - otherwise-working build. + """Export one tracked base ``uv.lock`` into a trusted hash-pinned closure. + + The sibling ``pyproject.toml`` determines whether the lock belongs to an + exportable project. Orphan locks are ignored, and a successful comment-only + export represents a valid project with no third-party dependency closure. + Every other exporter failure is fatal: silently dropping a tracked project + lock would execute coverage without the base dependencies and could turn + import failures into misleading review feedback. """ - uv_path = shutil.which("uv") - if uv_path is None: - return None project_dir = pathlib.PurePosixPath(lock_path).parent pyproject_path = ( "pyproject.toml" if str(project_dir) == "." else f"{project_dir}/pyproject.toml" ) + lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}") try: - lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}") pyproject_content = _git(repo_root, "show", f"{base_sha}:{pyproject_path}") except RuntimeError: return None + + uv_path = _install_trusted_uv() + with tempfile.TemporaryDirectory() as work_dir: work_path = pathlib.Path(work_dir) (work_path / "uv.lock").write_bytes(lock_content) (work_path / "pyproject.toml").write_bytes(pyproject_content) try: completed = _run_uv_export(work_path, uv_path) - except (OSError, subprocess.TimeoutExpired): - return None + except (OSError, subprocess.TimeoutExpired) as exc: + raise RuntimeError( + f"could not run trusted uv export for tracked base lock {lock_path}: " + f"{type(exc).__name__}" + ) from exc + if completed.returncode != 0: - return None + stderr = completed.stderr.decode("utf-8", errors="replace") + normalized_stderr = " ".join(stderr.split()) + detail = ( + normalized_stderr[:500] + if normalized_stderr + else f"exit status {completed.returncode}" + ) + raise RuntimeError( + f"uv export failed for tracked base lock {lock_path}: {detail}" + ) + exported = completed.stdout - return exported if _is_hash_pinned(exported) else None + if not _requirement_lines(exported): + return None + if not _is_hash_pinned(exported): + raise RuntimeError( + f"uv export for tracked base lock {lock_path} was not fully hash-pinned" + ) + return exported def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 41b86b261..fd2b68f1a 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -1,8 +1,11 @@ from __future__ import annotations +import hashlib +import io import runpy import subprocess import sys +import tarfile from pathlib import Path import pytest @@ -378,7 +381,7 @@ def test_uv_lock_is_exported_to_a_hash_pinned_lock( ) -> None: """A base uv.lock is exported via uv into a materialized hash-pinned closure.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") hashed = b"demo-dep==1 --hash=sha256:" + b"a" * 64 + b"\n" monkeypatch.setattr( materializer, @@ -393,14 +396,19 @@ def test_uv_lock_is_exported_to_a_hash_pinned_lock( assert (output / "requirements-000.txt").read_bytes() == hashed -def test_uv_lock_skipped_when_uv_is_unavailable( +def test_uv_lock_fails_closed_when_trusted_uv_bootstrap_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Without the uv exporter, a uv.lock-only repo materializes nothing (no regression).""" + """A tracked project uv.lock cannot silently lose its dependency evidence.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: None) - assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] + def fail_install() -> str: + raise RuntimeError("trusted uv bootstrap failed") + + monkeypatch.setattr(materializer, "_install_trusted_uv", fail_install) + + with pytest.raises(RuntimeError, match="trusted uv bootstrap failed"): + materializer.materialize(repo, base_sha, tmp_path / "output") def test_uv_lock_skipped_when_pyproject_is_absent( @@ -408,41 +416,327 @@ def test_uv_lock_skipped_when_pyproject_is_absent( ) -> None: """A uv.lock without a sibling pyproject.toml at base (in a subdir) cannot be exported.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=False, lock_dir="service") - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + def unexpected_install() -> str: + raise AssertionError("orphan uv.lock must not bootstrap uv") + + monkeypatch.setattr(materializer, "_install_trusted_uv", unexpected_install) assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] -def test_uv_lock_skipped_when_export_fails( +def test_uv_lock_fails_closed_when_export_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """A non-zero uv export (e.g. a stale lock) is skipped, never materialized.""" + """A stale or otherwise unexportable tracked uv.lock blocks evidence creation.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") monkeypatch.setattr( materializer, "_run_uv_export", - lambda _work, _uv_path: _export(1, b""), + lambda _work, _uv_path: subprocess.CompletedProcess( + ["uv", "export"], 1, b"", b"lock is stale\n" + ), ) - assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] + with pytest.raises(RuntimeError, match="uv export failed.*lock is stale"): + materializer.materialize(repo, base_sha, tmp_path / "output") -def test_uv_lock_skipped_when_export_is_not_hash_pinned( +def test_uv_lock_fails_closed_when_export_is_not_hash_pinned( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """A uv export that somehow lacks hashes is rejected by the hash-pin guard.""" + """A nonempty uv export without hashes is rejected instead of ignored.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") monkeypatch.setattr( materializer, "_run_uv_export", lambda _work, _uv_path: _export(0, b"unpinned==1\n"), ) + with pytest.raises(RuntimeError, match="not fully hash-pinned"): + materializer.materialize(repo, base_sha, tmp_path / "output") + + +def test_uv_lock_with_empty_dependency_closure_materializes_nothing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A successful comment-only uv export represents a valid empty closure.""" + repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") + monkeypatch.setattr( + materializer, + "_run_uv_export", + lambda _work, _uv_path: _export(0, b"# no third-party dependencies\n"), + ) + assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] +class _FakeResponse: + """Minimal context-managed HTTP response used by trusted uv download tests.""" + + def __init__(self, payload: bytes, final_url: str) -> None: + """Store deterministic response bytes and the observed final URL.""" + self._payload = payload + self._final_url = final_url + + def __enter__(self) -> "_FakeResponse": + """Return this response from a context manager.""" + return self + + def __exit__(self, *_args: object) -> None: + """Close the fake response without suppressing exceptions.""" + + def geturl(self) -> str: + """Return the final URL after redirects.""" + return self._final_url + + def read(self, size: int) -> bytes: + """Return at most ``size`` bytes like an HTTP response.""" + return self._payload[:size] + + +def _trusted_uv_archive( + binary: bytes = b"verified-uv", + *, + member_name: str = materializer.TRUSTED_UV_ARCHIVE_MEMBER, + regular: bool = True, +) -> bytes: + """Build a deterministic uv tar archive for supply-chain boundary tests.""" + payload = io.BytesIO() + with tarfile.open(fileobj=payload, mode="w:gz") as bundle: + member = tarfile.TarInfo(member_name) + if regular: + member.size = len(binary) + bundle.addfile(member, io.BytesIO(binary)) + else: + member.type = tarfile.DIRTYPE + bundle.addfile(member) + return payload.getvalue() + + +def test_download_trusted_uv_archive_accepts_fixed_https_origin( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The downloader returns bounded bytes from the fixed Astral HTTPS origin.""" + payload = b"archive" + response = _FakeResponse(payload, materializer.TRUSTED_UV_ARCHIVE_URL) + monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + + assert materializer._download_trusted_uv_archive() == payload + + +def test_download_trusted_uv_archive_rejects_unsafe_redirect( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A redirect away from the fixed HTTPS release host fails closed.""" + response = _FakeResponse(b"archive", "https://example.invalid/uv.tar.gz") + monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + + with pytest.raises(RuntimeError, match="redirected outside"): + materializer._download_trusted_uv_archive() + + +def test_download_trusted_uv_archive_rejects_network_and_size_failures( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Network errors and oversized archives cannot enter the trusted tool path.""" + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_a, **_k: (_ for _ in ()).throw(OSError("offline")), + ) + with pytest.raises(RuntimeError, match="download failed"): + materializer._download_trusted_uv_archive() + + response = _FakeResponse(b"12345", materializer.TRUSTED_UV_ARCHIVE_URL) + monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + monkeypatch.setattr(materializer, "TRUSTED_UV_DOWNLOAD_MAX_BYTES", 4) + with pytest.raises(RuntimeError, match="bounded download size"): + materializer._download_trusted_uv_archive() + + +def test_verified_uv_binary_accepts_exact_archive( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An exact-hash archive yields only its bounded regular uv member.""" + archive = _trusted_uv_archive() + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest() + ) + + assert materializer._verified_uv_binary(archive) == b"verified-uv" + + +@pytest.mark.parametrize( + ("archive", "error"), + [ + (b"not-a-tar", "checksum verification failed"), + (_trusted_uv_archive(member_name="wrong/uv"), "omitted the uv executable"), + (_trusted_uv_archive(regular=False), "not a regular file"), + ], +) +def test_verified_uv_binary_rejects_invalid_archives( + archive: bytes, error: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """Checksum, membership, and file-type violations fail closed.""" + if error != "checksum verification failed": + monkeypatch.setattr( + materializer, + "TRUSTED_UV_ARCHIVE_SHA256", + hashlib.sha256(archive).hexdigest(), + ) + with pytest.raises(RuntimeError, match=error): + materializer._verified_uv_binary(archive) + + +def test_verified_uv_binary_rejects_parse_and_size_failures( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Corrupt tar data and oversized executable metadata cannot be installed.""" + corrupt = b"not-a-tar" + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(corrupt).hexdigest() + ) + with pytest.raises(RuntimeError, match="could not be parsed"): + materializer._verified_uv_binary(corrupt) + + archive = _trusted_uv_archive(binary=b"large") + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest() + ) + monkeypatch.setattr(materializer, "TRUSTED_UV_BINARY_MAX_BYTES", 4) + with pytest.raises(RuntimeError, match="bounded size"): + materializer._verified_uv_binary(archive) + + +def test_verified_uv_binary_rejects_truncated_member( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A truncated regular member cannot satisfy the archive size receipt.""" + archive = b"archive" + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest() + ) + + class _Member: + """Represent one regular member with a longer declared size.""" + + size = 2 + + @staticmethod + def isfile() -> bool: + """Return that this synthetic member is regular.""" + return True + + class _Bundle: + """Return a deliberately truncated member stream.""" + + def __enter__(self) -> "_Bundle": + """Enter the synthetic archive context.""" + return self + + def __exit__(self, *_args: object) -> None: + """Leave the synthetic archive context.""" + + @staticmethod + def getmember(_name: str) -> _Member: + """Return the synthetic regular member.""" + return _Member() + + @staticmethod + def extractfile(_member: _Member) -> io.BytesIO: + """Return fewer bytes than the member metadata declares.""" + return io.BytesIO(b"x") + + monkeypatch.setattr(materializer.tarfile, "open", lambda *_a, **_k: _Bundle()) + + with pytest.raises(RuntimeError, match="size did not match"): + materializer._verified_uv_binary(archive) + + +def test_install_trusted_uv_verifies_version_and_caches_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The installer writes one executable, verifies its version, and caches it.""" + materializer._install_trusted_uv.cache_clear() + monkeypatch.setattr(materializer.tempfile, "mkdtemp", lambda **_k: str(tmp_path / "uv")) + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", lambda: b"archive") + monkeypatch.setattr(materializer, "_verified_uv_binary", lambda _payload: b"binary") + registered: list[tuple[object, ...]] = [] + monkeypatch.setattr(materializer.atexit, "register", lambda *args, **_kwargs: registered.append(args)) + calls = 0 + + def verify(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[bytes]: + nonlocal calls + calls += 1 + return subprocess.CompletedProcess([], 0, b"uv 0.12.1\n", b"") + + monkeypatch.setattr(materializer.subprocess, "run", verify) + + first = materializer._install_trusted_uv() + second = materializer._install_trusted_uv() + + assert first == second == str(tmp_path / "uv" / "uv") + assert Path(first).read_bytes() == b"binary" + assert Path(first).stat().st_mode & 0o111 + assert calls == 1 + assert registered + materializer._install_trusted_uv.cache_clear() + + +@pytest.mark.parametrize( + "failure", + [ + FileNotFoundError("missing binary"), + subprocess.TimeoutExpired(["uv", "--version"], timeout=10), + ], +) +def test_install_trusted_uv_rejects_version_process_failures( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + failure: OSError | subprocess.TimeoutExpired, +) -> None: + """A missing or hung downloaded executable is removed and rejected.""" + materializer._install_trusted_uv.cache_clear() + tool_dir = tmp_path / "uv" + monkeypatch.setattr(materializer.tempfile, "mkdtemp", lambda **_k: str(tool_dir)) + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", lambda: b"archive") + monkeypatch.setattr(materializer, "_verified_uv_binary", lambda _payload: b"binary") + + def fail(*_args: object, **_kwargs: object) -> None: + raise failure + + monkeypatch.setattr(materializer.subprocess, "run", fail) + with pytest.raises(RuntimeError, match="executable verification failed"): + materializer._install_trusted_uv() + assert not tool_dir.exists() + materializer._install_trusted_uv.cache_clear() + + +def test_install_trusted_uv_rejects_wrong_version_or_exit_status( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Unexpected version output or a nonzero status cannot satisfy the pin.""" + for completed in ( + subprocess.CompletedProcess([], 0, b"uv 0.12.0\n", b""), + subprocess.CompletedProcess([], 1, b"uv 0.12.1\n", b"failed"), + ): + materializer._install_trusted_uv.cache_clear() + tool_dir = tmp_path / f"uv-{completed.returncode}-{len(completed.stdout)}" + monkeypatch.setattr( + materializer.tempfile, "mkdtemp", lambda **_k: str(tool_dir) + ) + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", lambda: b"archive") + monkeypatch.setattr(materializer, "_verified_uv_binary", lambda _payload: b"binary") + monkeypatch.setattr(materializer.subprocess, "run", lambda *_a, **_k: completed) + with pytest.raises(RuntimeError, match="unexpected version or exit status"): + materializer._install_trusted_uv() + assert not tool_dir.exists() + materializer._install_trusted_uv.cache_clear() + + def test_run_uv_export_invokes_uv_with_frozen_offline_flags( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -475,18 +769,19 @@ def fake_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[b subprocess.TimeoutExpired(["/usr/bin/uv", "export"], timeout=120), ], ) -def test_uv_export_process_failures_fall_back_to_no_lock( +def test_uv_export_process_failures_fail_closed( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, export_error: OSError | subprocess.TimeoutExpired, ) -> None: - """A missing or hung uv process preserves the documented best-effort fallback.""" + """A missing or hung trusted uv process cannot silently drop dependencies.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") def fail_export(_work: Path, _uv_path: str) -> None: raise export_error monkeypatch.setattr(materializer, "_run_uv_export", fail_export) - assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] + with pytest.raises(RuntimeError, match="could not run trusted uv export"): + materializer.materialize(repo, base_sha, tmp_path / "output") From 59beb4bb1aa14d22fe373d99f36a83e0ac0da124 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:20:32 +0900 Subject: [PATCH 02/20] fix(security): make trusted uv download URL statically provable --- scripts/ci/materialize_base_python_requirements.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 1a1dd87f7..78a22d094 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -102,14 +102,14 @@ def _git(repo_root: pathlib.Path, *args: str) -> bytes: def _download_trusted_uv_archive() -> bytes: """Download the fixed uv release archive through one HTTPS trust boundary.""" - request = urllib.request.Request( - TRUSTED_UV_ARCHIVE_URL, - headers={"User-Agent": "ContextualWisdomLab-coverage/1"}, - method="GET", - ) try: - with urllib.request.urlopen( # nosec B310 -- fixed URL plus SHA-256 pin - request, timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS + # Keep the audited URL literal at the network sink so static analysis can + # prove that neither user data nor repository content selects a scheme, + # host, path, query, fragment, method, or request header. + with urllib.request.urlopen( # nosec B310 -- literal HTTPS URL plus SHA pin + "https://releases.astral.sh/github/uv/releases/download/0.12.1/" + "uv-x86_64-unknown-linux-gnu.tar.gz", + timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS, ) as response: final_url = urllib.parse.urlparse(response.geturl()) if (final_url.scheme, final_url.hostname) != ( From e57ef51c75bdd6e9a35f521b24dfe3ff9b0d5592 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:26:45 +0900 Subject: [PATCH 03/20] test(security): pin the trusted uv URL sink contract --- tests/test_trusted_uv_download_contract.py | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_trusted_uv_download_contract.py diff --git a/tests/test_trusted_uv_download_contract.py b/tests/test_trusted_uv_download_contract.py new file mode 100644 index 000000000..614182045 --- /dev/null +++ b/tests/test_trusted_uv_download_contract.py @@ -0,0 +1,53 @@ +"""Static security contract for the pinned trusted-uv network boundary.""" + +from __future__ import annotations + +from pathlib import Path + + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_MATERIALIZER = _REPO_ROOT / "scripts" / "ci" / "materialize_base_python_requirements.py" +_EXPECTED_URL = ( + "https://releases.astral.sh/github/uv/releases/download/0.12.1/" + "uv-x86_64-unknown-linux-gnu.tar.gz" +) + + +def _download_function_text() -> str: + """Return only the trusted-uv download function source.""" + module_text = _MATERIALIZER.read_text(encoding="utf-8") + return module_text.split( + "def _download_trusted_uv_archive() -> bytes:", maxsplit=1 + )[1].split("def _verified_uv_binary", maxsplit=1)[0] + + +def test_urlopen_receives_one_literal_https_release_url() -> None: + """Static analysis can prove repository or user data never selects the URL.""" + function_text = _download_function_text() + + assert "urllib.request.Request" not in function_text + assert function_text.count("urllib.request.urlopen(") == 1 + assert '"https://releases.astral.sh/github/uv/releases/download/0.12.1/"' in function_text + assert '"uv-x86_64-unknown-linux-gnu.tar.gz"' in function_text + assert "TRUSTED_UV_ARCHIVE_URL" not in function_text + + +def test_literal_network_sink_matches_the_documented_release_constant() -> None: + """The scanner-friendly literal cannot drift from the tested release identity.""" + module_text = _MATERIALIZER.read_text(encoding="utf-8") + namespace: dict[str, object] = {} + constant_block = module_text.split( + "TRUSTED_UV_ARCHIVE_URL = (", maxsplit=1 + )[1].split(")", maxsplit=1)[0] + + exec("TRUSTED_UV_ARCHIVE_URL = (" + constant_block + ")", {}, namespace) + + assert namespace["TRUSTED_UV_ARCHIVE_URL"] == _EXPECTED_URL + function_text = _download_function_text() + assert _EXPECTED_URL == "".join( + ( + "https://releases.astral.sh/github/uv/releases/download/0.12.1/", + "uv-x86_64-unknown-linux-gnu.tar.gz", + ) + ) + assert all(part in function_text for part in _EXPECTED_URL.rsplit("/", maxsplit=1)) From 635bd7dd8859cb95b592c6d66ecb11628a985e13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:27:15 +0900 Subject: [PATCH 04/20] test(security): parse the trusted uv URL contract without code execution --- tests/test_trusted_uv_download_contract.py | 86 ++++++++++++++-------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/tests/test_trusted_uv_download_contract.py b/tests/test_trusted_uv_download_contract.py index 614182045..f227ed93c 100644 --- a/tests/test_trusted_uv_download_contract.py +++ b/tests/test_trusted_uv_download_contract.py @@ -2,6 +2,7 @@ from __future__ import annotations +import ast from pathlib import Path @@ -13,41 +14,66 @@ ) -def _download_function_text() -> str: - """Return only the trusted-uv download function source.""" - module_text = _MATERIALIZER.read_text(encoding="utf-8") - return module_text.split( - "def _download_trusted_uv_archive() -> bytes:", maxsplit=1 - )[1].split("def _verified_uv_binary", maxsplit=1)[0] +def _module_tree() -> ast.Module: + """Parse the materializer without importing or executing repository code.""" + return ast.parse(_MATERIALIZER.read_text(encoding="utf-8"), filename=str(_MATERIALIZER)) + + +def _download_function() -> ast.FunctionDef: + """Return the trusted-uv downloader function from the parsed module.""" + for node in _module_tree().body: + if isinstance(node, ast.FunctionDef) and node.name == "_download_trusted_uv_archive": + return node + raise AssertionError("trusted uv downloader function is missing") + + +def _assigned_literal(name: str) -> object: + """Return one module-level literal assignment without evaluating code.""" + for node in _module_tree().body: + if not isinstance(node, ast.Assign) or len(node.targets) != 1: + continue + target = node.targets[0] + if isinstance(target, ast.Name) and target.id == name: + return ast.literal_eval(node.value) + raise AssertionError(f"module literal {name} is missing") + + +def _urlopen_calls() -> list[ast.Call]: + """Return calls whose attribute name is exactly ``urlopen``.""" + return [ + node + for node in ast.walk(_download_function()) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "urlopen" + ] def test_urlopen_receives_one_literal_https_release_url() -> None: """Static analysis can prove repository or user data never selects the URL.""" - function_text = _download_function_text() + calls = _urlopen_calls() - assert "urllib.request.Request" not in function_text - assert function_text.count("urllib.request.urlopen(") == 1 - assert '"https://releases.astral.sh/github/uv/releases/download/0.12.1/"' in function_text - assert '"uv-x86_64-unknown-linux-gnu.tar.gz"' in function_text - assert "TRUSTED_UV_ARCHIVE_URL" not in function_text + assert len(calls) == 1 + assert len(calls[0].args) == 1 + url_argument = calls[0].args[0] + assert isinstance(url_argument, ast.Constant) + assert isinstance(url_argument.value, str) + assert url_argument.value == _EXPECTED_URL def test_literal_network_sink_matches_the_documented_release_constant() -> None: - """The scanner-friendly literal cannot drift from the tested release identity.""" - module_text = _MATERIALIZER.read_text(encoding="utf-8") - namespace: dict[str, object] = {} - constant_block = module_text.split( - "TRUSTED_UV_ARCHIVE_URL = (", maxsplit=1 - )[1].split(")", maxsplit=1)[0] - - exec("TRUSTED_UV_ARCHIVE_URL = (" + constant_block + ")", {}, namespace) - - assert namespace["TRUSTED_UV_ARCHIVE_URL"] == _EXPECTED_URL - function_text = _download_function_text() - assert _EXPECTED_URL == "".join( - ( - "https://releases.astral.sh/github/uv/releases/download/0.12.1/", - "uv-x86_64-unknown-linux-gnu.tar.gz", - ) - ) - assert all(part in function_text for part in _EXPECTED_URL.rsplit("/", maxsplit=1)) + """The scanner-friendly sink literal cannot drift from the release identity.""" + assert _assigned_literal("TRUSTED_UV_ARCHIVE_URL") == _EXPECTED_URL + + +def test_downloader_never_constructs_a_dynamic_request_object() -> None: + """The audited downloader cannot hide a dynamic URL inside ``Request``.""" + request_calls = [ + node + for node in ast.walk(_download_function()) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "Request" + ] + + assert request_calls == [] From 2f6300cdbcd37297b39a4cdb72d453787ef2a37b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:28:05 +0900 Subject: [PATCH 05/20] test(coverage): require per-dependency hashes from uv export --- ...est_materialize_uv_export_hash_contract.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_materialize_uv_export_hash_contract.py diff --git a/tests/test_materialize_uv_export_hash_contract.py b/tests/test_materialize_uv_export_hash_contract.py new file mode 100644 index 000000000..9d88b4870 --- /dev/null +++ b/tests/test_materialize_uv_export_hash_contract.py @@ -0,0 +1,38 @@ +"""Fail-closed hash validation for trusted ``uv export`` output.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +def test_uv_export_requires_a_hash_on_each_requirement( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A global require-hashes directive cannot replace per-requirement hashes.""" + + def fake_git(_repo_root: Path, *args: str) -> bytes: + assert args[0] == "show" + return b"version = 1\n" if args[1].endswith(":uv.lock") else b"[project]\n" + + malformed_export = b"--require-hashes\ndemo==1\n" + monkeypatch.setattr(materializer, "_git", fake_git) + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/trusted/uv") + monkeypatch.setattr( + materializer, + "_run_uv_export", + lambda _work_dir, _uv_path: subprocess.CompletedProcess( + ["uv", "export"], + 0, + malformed_export, + b"", + ), + ) + + with pytest.raises(RuntimeError, match="not fully hash-pinned"): + materializer._export_uv_lock(tmp_path, "a" * 40, "uv.lock") From 59ad18529e7685876fbdf4382d543bbfbbd0c25e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:29:35 +0900 Subject: [PATCH 06/20] fix(coverage): require hashes on every uv export requirement --- .../ci/materialize_base_python_requirements.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 78a22d094..8ded1b0c5 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -86,6 +86,20 @@ def _is_hash_pinned(content: bytes) -> bool: ) +def _is_fully_hash_pinned_export(content: bytes) -> bool: + """Return whether every emitted uv requirement carries its own hash. + + The fixed exporter invocation does not request index, find-links, binary, or + global hash directives. Therefore every non-comment logical line must be one + concrete requirement with at least one ``--hash=`` value. This stricter check + is intentionally separate from generic requirements-file discovery, where a + global ``--require-hashes`` directive is still safe to pass to pip's later + closure preflight. + """ + lines = _requirement_lines(content) + return bool(lines) and all("--hash=" in line for line in lines) + + def _git(repo_root: pathlib.Path, *args: str) -> bytes: """Run one read-only git command in the materialized repository.""" completed = subprocess.run( @@ -278,7 +292,7 @@ def _export_uv_lock( exported = completed.stdout if not _requirement_lines(exported): return None - if not _is_hash_pinned(exported): + if not _is_fully_hash_pinned_export(exported): raise RuntimeError( f"uv export for tracked base lock {lock_path} was not fully hash-pinned" ) From 4a7a01d07d1470ba8bb4ac00257c8e9925f914d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:31:52 +0900 Subject: [PATCH 07/20] docs(doctoring): record trusted uv materialization evidence --- .../trusted-uv-lock-materialization.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/doctoring/trusted-uv-lock-materialization.md diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md new file mode 100644 index 000000000..7c6bac4a4 --- /dev/null +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -0,0 +1,77 @@ +# Trusted `uv.lock` materialization: evidence and design record + +## Decision + +Central coverage automation may translate a tracked `uv.lock` from the exact +validated pull-request base revision into a pip-compatible, hash-pinned +requirements closure. The translation must not depend on a mutable runner tool, +repository-head dependency metadata, or network access during export. + +The implementation therefore: + +1. reads `uv.lock` and its sibling `pyproject.toml` only through `git show` at a + validated 40-character commit SHA; +2. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; +3. verifies the bounded archive with a pinned SHA-256 digest before extraction; +4. accepts only the expected regular-file tar member within explicit size bounds; +5. verifies the installed executable reports the exact pinned `uv` version; +6. executes `uv export` with `--frozen`, `--offline`, `--no-emit-project`, and + `--no-editable` in an isolated temporary project; +7. rejects every nonempty export unless every logical requirement carries an + explicit `--hash=` value; and +8. exposes only generated requirements files and a source manifest to the later + networkless coverage environment. + +## Standards and current-tool rationale + +The approved SLSA specification is version 1.2. Its provenance model treats +verifiable origin and production history as software-supply-chain evidence, and +its source track distinguishes trusted robots whose identity and codebase cannot +be unilaterally influenced. Binding reads to an immutable Git revision, pinning +the exporter artifact by digest, and rejecting malformed exporter output follow +that trust-minimization direction without claiming a SLSA conformance level. + +Astral documents `uv export` as the supported conversion path from `uv.lock` to a +pip-compatible requirements format. The command is invoked with `--frozen` so it +cannot update the lock and `--offline` so the conversion cannot access the +network. Project and editable entries are omitted because the coverage sandbox +loads repository source directly and needs only the third-party dependency +closure. + +Generic requirements discovery continues to accept a global +`--require-hashes` directive because pip performs a later closure preflight. +Trusted `uv export` output uses a stricter rule: each emitted requirement must +carry its own hash. This prevents a successful but malformed exporter result +such as `--require-hashes` followed by an unhashed requirement from entering the +trusted build context. + +## Verification contract + +Regression coverage must prove: + +- base-revision-only reads and rejection of unsafe revision/path shapes; +- fixed-origin download, redirect rejection, bounded reads, archive digest, + member type, member size, executable size, and exact executable version; +- frozen and offline exporter arguments; +- timeout, process, parse, and exporter failures fail closed; +- orphan locks and empty third-party closures remain nonfatal and explicit; +- every nonempty emitted requirement includes a hash; and +- the changed production module retains 100% statement and branch coverage and + 100% production docstrings. + +## References + +Astral Software, Inc. (n.d.). *Exporting a lockfile*. uv documentation. Retrieved +August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/export/ + +Astral Software, Inc. (n.d.). *Locking and syncing*. uv documentation. Retrieved +August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/sync/ + +Supply-chain Levels for Software Artifacts. (2026). *SLSA specification +(version 1.2)*. https://slsa.dev/spec/v1.2/ + +Supply-chain Levels for Software Artifacts. (2026). *Provenance (version 1.2)*. +https://slsa.dev/spec/v1.2/provenance + +Supply-chain Levels for Software Artifacts. (2026). *Source: Requirements for +producing source (version 1.2)*. https://slsa.dev/spec/v1.2/source-requirements From 7a2528e4e54d9f5306ecaa9a8febceec11152b1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:54:32 +0900 Subject: [PATCH 08/20] test(coverage): require isolated strict uv export --- tests/test_uv_export_isolation_contract.py | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/test_uv_export_isolation_contract.py diff --git a/tests/test_uv_export_isolation_contract.py b/tests/test_uv_export_isolation_contract.py new file mode 100644 index 000000000..04da0535c --- /dev/null +++ b/tests/test_uv_export_isolation_contract.py @@ -0,0 +1,97 @@ +"""Behavioral isolation and output contracts for trusted ``uv export``.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +def test_uv_export_runs_with_a_bounded_isolated_environment( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ambient runner configuration cannot select export behavior or cache state.""" + observed: dict[str, object] = {} + + def fake_run(command: list[str], **kwargs): + observed["command"] = command + observed["kwargs"] = kwargs + return subprocess.CompletedProcess(command, 0, b"", b"") + + monkeypatch.setattr(materializer.subprocess, "run", fake_run) + + result = materializer._run_uv_export(tmp_path, "/trusted/uv") + + assert result.returncode == 0 + assert observed["command"] == [ + "/trusted/uv", + "export", + "--frozen", + "--offline", + "--no-cache", + "--no-progress", + "--color", + "never", + "--no-emit-project", + "--no-editable", + "--format", + "requirements-txt", + ] + kwargs = observed["kwargs"] + assert isinstance(kwargs, dict) + assert kwargs["cwd"] == str(tmp_path) + assert kwargs["check"] is False + assert kwargs["stdout"] is subprocess.PIPE + assert kwargs["stderr"] is subprocess.PIPE + + environment = kwargs["env"] + assert environment == { + "HOME": str(tmp_path / ".uv-home"), + "NO_COLOR": "1", + "PATH": os.defpath, + "TMPDIR": str(tmp_path / ".uv-tmp"), + "UV_NO_ENV_FILE": "1", + "UV_PYTHON_DOWNLOADS": "never", + "XDG_CACHE_HOME": str(tmp_path / ".uv-cache"), + "XDG_CONFIG_HOME": str(tmp_path / ".uv-config"), + } + for directory_name in (".uv-home", ".uv-tmp", ".uv-cache", ".uv-config"): + assert (tmp_path / directory_name).is_dir() + + +def test_uv_export_does_not_disable_project_metadata_discovery() -> None: + """Isolation must retain the reconstructed project's ``pyproject.toml`` input.""" + source = Path(materializer.__file__).read_text(encoding="utf-8") + + assert '"--no-config"' not in source + assert "UV_NO_CONFIG" not in source + + +@pytest.mark.parametrize( + "content", + [ + b"--index-url https://packages.invalid/simple --hash=sha256:" + b"a" * 64 + b"\n", + b"demo @ file:///tmp/demo --hash=sha256:" + b"a" * 64 + b"\n", + b"demo==1 --hash=sha512:" + b"a" * 128 + b"\n", + b"demo==1 --hash=sha256:abcd\n", + ], +) +def test_uv_export_rejects_non_package_or_non_sha256_lines(content: bytes) -> None: + """An option, local reference, wrong algorithm, or short digest is not a lock pin.""" + assert materializer._is_fully_hash_pinned_export(content) is False + + +def test_uv_export_accepts_exact_package_pins_with_markers_and_multiple_hashes() -> None: + """A normalized exact requirement with SHA-256 hashes remains exportable.""" + content = ( + b"demo-extra[fast]==1.2.3 ; python_version >= '3.12' \\\n" + b" --hash=sha256:" + b"a" * 64 + b" \\\n" + b" --hash=sha256:" + b"b" * 64 + b"\n" + ) + + assert materializer._is_fully_hash_pinned_export(content) is True From ddd9d8e09d3ded6a6494c4c6fe752bf150777e69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 20:57:47 +0900 Subject: [PATCH 09/20] fix(coverage): isolate and strictly validate uv export --- .../materialize_base_python_requirements.py | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 8ded1b0c5..891b3ce31 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -10,6 +10,7 @@ import hashlib import io import json +import os import pathlib import re import shutil @@ -22,6 +23,12 @@ SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") +UV_EXACT_REQUIREMENT_RE = re.compile( + r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?" + r"(?:\[[A-Za-z0-9._-]+(?:,[A-Za-z0-9._-]+)*\])?" + r"==[^\s;]+(?:\s*;\s*\S(?:.*\S)?)?" +) +UV_SHA256_HASH_RE = re.compile(r"--hash=sha256:[0-9a-fA-F]{64}") UV_EXPORT_TIMEOUT_SECONDS = 120 TRUSTED_UV_VERSION = "0.12.1" TRUSTED_UV_ARCHIVE_URL = ( @@ -86,18 +93,28 @@ def _is_hash_pinned(content: bytes) -> bool: ) +def _is_fully_hash_pinned_requirement(line: str) -> bool: + """Return whether one uv-export line is an exact package pin with SHA-256 hashes.""" + fields = re.split(r"\s+(?=--hash=)", line) + if len(fields) < 2: + return False + requirement, *hashes = fields + if UV_EXACT_REQUIREMENT_RE.fullmatch(requirement) is None: + return False + return all(UV_SHA256_HASH_RE.fullmatch(hash_value) for hash_value in hashes) + + def _is_fully_hash_pinned_export(content: bytes) -> bool: - """Return whether every emitted uv requirement carries its own hash. + """Return whether every emitted uv requirement is exactly SHA-256 pinned. The fixed exporter invocation does not request index, find-links, binary, or - global hash directives. Therefore every non-comment logical line must be one - concrete requirement with at least one ``--hash=`` value. This stricter check - is intentionally separate from generic requirements-file discovery, where a - global ``--require-hashes`` directive is still safe to pass to pip's later - closure preflight. + global hash directives. Every non-comment logical line must therefore be one + normalized package ``==`` pin with at least one complete SHA-256 hash. Option + lines, local/direct references, other algorithms, and truncated hashes are + rejected even when they contain a ``--hash=`` substring. """ lines = _requirement_lines(content) - return bool(lines) and all("--hash=" in line for line in lines) + return bool(lines) and all(_is_fully_hash_pinned_requirement(line) for line in lines) def _git(repo_root: pathlib.Path, *args: str) -> bytes: @@ -206,6 +223,28 @@ def _install_trusted_uv() -> str: return str(uv_path) +def _trusted_uv_export_environment(work_dir: pathlib.Path) -> dict[str, str]: + """Create the minimal deterministic environment allowed to influence uv export.""" + directories = { + "HOME": work_dir / ".uv-home", + "TMPDIR": work_dir / ".uv-tmp", + "XDG_CACHE_HOME": work_dir / ".uv-cache", + "XDG_CONFIG_HOME": work_dir / ".uv-config", + } + for directory in directories.values(): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + return { + "HOME": str(directories["HOME"]), + "NO_COLOR": "1", + "PATH": os.defpath, + "TMPDIR": str(directories["TMPDIR"]), + "UV_NO_ENV_FILE": "1", + "UV_PYTHON_DOWNLOADS": "never", + "XDG_CACHE_HOME": str(directories["XDG_CACHE_HOME"]), + "XDG_CONFIG_HOME": str(directories["XDG_CONFIG_HOME"]), + } + + def _run_uv_export( work_dir: pathlib.Path, uv_path: str, @@ -214,11 +253,11 @@ def _run_uv_export( ) -> subprocess.CompletedProcess[bytes]: """Run ``uv export`` for a reconstructed base project and return the result. - ``--frozen`` forbids lock mutation and ``--offline`` forbids network access, - so the export is a pure function of the already-trusted base ``uv.lock`` and - ``pyproject.toml``; ``--no-emit-project``/``--no-editable`` drop the project - itself (installed via ``PYTHONPATH`` in the sandbox) and keep only its - hash-pinned dependency closure. + ``--frozen`` forbids lock mutation and ``--offline`` forbids network access. + A minimal environment and ephemeral cache/config/home directories prevent + runner-level configuration, dotenv files, Python downloads, or persistent + cache state from selecting export behavior. Project metadata discovery stays + enabled so the reconstructed ``pyproject.toml`` remains authoritative. """ return subprocess.run( [ @@ -226,6 +265,10 @@ def _run_uv_export( "export", "--frozen", "--offline", + "--no-cache", + "--no-progress", + "--color", + "never", "--no-emit-project", "--no-editable", "--format", @@ -236,6 +279,7 @@ def _run_uv_export( stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout, + env=_trusted_uv_export_environment(work_dir), ) From 229711dbeed51228c5ddb873115a258e66407ffd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:01:24 +0900 Subject: [PATCH 10/20] docs(doctoring): record isolated uv export boundary --- .../trusted-uv-lock-materialization.md | 93 +++++++++++++------ 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md index 7c6bac4a4..fc840838c 100644 --- a/docs/doctoring/trusted-uv-lock-materialization.md +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -5,7 +5,8 @@ Central coverage automation may translate a tracked `uv.lock` from the exact validated pull-request base revision into a pip-compatible, hash-pinned requirements closure. The translation must not depend on a mutable runner tool, -repository-head dependency metadata, or network access during export. +repository-head dependency metadata, ambient runner configuration, or network +access during export. The implementation therefore: @@ -14,13 +15,21 @@ The implementation therefore: 2. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; 3. verifies the bounded archive with a pinned SHA-256 digest before extraction; 4. accepts only the expected regular-file tar member within explicit size bounds; -5. verifies the installed executable reports the exact pinned `uv` version; -6. executes `uv export` with `--frozen`, `--offline`, `--no-emit-project`, and - `--no-editable` in an isolated temporary project; -7. rejects every nonempty export unless every logical requirement carries an - explicit `--hash=` value; and -8. exposes only generated requirements files and a source manifest to the later - networkless coverage environment. +5. writes the executable with mode `0755` and verifies that it reports the exact + pinned `uv` version; +6. executes `uv export` with `--frozen`, `--offline`, `--no-cache`, + `--no-progress`, `--color never`, `--no-emit-project`, and `--no-editable` in + an isolated temporary project; +7. supplies a minimal environment with isolated home, temporary, cache, and + configuration directories, disables dotenv loading and managed Python + downloads, and does not inherit arbitrary runner variables; +8. keeps project metadata discovery enabled because the reconstructed + `pyproject.toml` is an authoritative input; `--no-config` is deliberately not + used because uv documents that it disables `pyproject.toml` discovery; +9. rejects every nonempty export unless every logical line is an exact normalized + package `==` pin followed only by complete SHA-256 hashes; and +10. exposes only generated requirements files and a source manifest to the later + networkless coverage environment. ## Standards and current-tool rationale @@ -28,22 +37,47 @@ The approved SLSA specification is version 1.2. Its provenance model treats verifiable origin and production history as software-supply-chain evidence, and its source track distinguishes trusted robots whose identity and codebase cannot be unilaterally influenced. Binding reads to an immutable Git revision, pinning -the exporter artifact by digest, and rejecting malformed exporter output follow -that trust-minimization direction without claiming a SLSA conformance level. +the exporter artifact by digest, isolating ambient configuration, and rejecting +malformed exporter output follow that trust-minimization direction without +claiming a SLSA conformance level. Astral documents `uv export` as the supported conversion path from `uv.lock` to a -pip-compatible requirements format. The command is invoked with `--frozen` so it -cannot update the lock and `--offline` so the conversion cannot access the -network. Project and editable entries are omitted because the coverage sandbox -loads repository source directly and needs only the third-party dependency -closure. +pip-compatible requirements format. Hashes are emitted by default. `--frozen` +prevents lock mutation, `--offline` prevents network access, and `--no-cache` +uses an ephemeral cache. Project and editable entries are omitted because the +coverage sandbox loads repository source directly and needs only the +third-party dependency closure. + +The global `--no-config` option is not appropriate here. uv documents that it +prevents discovery of both `pyproject.toml` and `uv.toml`. The materializer +instead isolates `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, and `TMPDIR`, +sets `UV_NO_ENV_FILE=1` and `UV_PYTHON_DOWNLOADS=never`, and passes only a fixed +`PATH`. This preserves the exact reconstructed project metadata while excluding +user-level and runner-level configuration state. Generic requirements discovery continues to accept a global `--require-hashes` directive because pip performs a later closure preflight. -Trusted `uv export` output uses a stricter rule: each emitted requirement must -carry its own hash. This prevents a successful but malformed exporter result -such as `--require-hashes` followed by an unhashed requirement from entering the -trusted build context. +Trusted `uv export` output uses a stricter rule: every logical line must begin +with a normalized package name and exact `==` pin, and every following hash must +be a complete `sha256` digest. Option lines, direct or local references, other +algorithms, truncated digests, and global directives are rejected even when they +contain a `--hash=` substring. + +## Modular and workspace boundary + +Nested standalone services are supported: a repository may contain several +independent directories, each with its own sibling `pyproject.toml` and +`uv.lock`; each pair is read and exported independently from the immutable base +revision. This fits the organization’s standalone-product plus reusable-module +MSA contract without copying central review logic into product repositories. + +A true uv workspace can require member `pyproject.toml` files in addition to the +root lock and root project metadata. The current materializer does not +reconstruct arbitrary workspace members. Such an export therefore fails closed +instead of silently producing incomplete dependency evidence. Workspace-member +reconstruction must be implemented as a separate bounded change that enumerates +member metadata from the same immutable base tree and proves `--all-packages` +and local-package omission semantics before it is enabled. ## Verification contract @@ -51,11 +85,14 @@ Regression coverage must prove: - base-revision-only reads and rejection of unsafe revision/path shapes; - fixed-origin download, redirect rejection, bounded reads, archive digest, - member type, member size, executable size, and exact executable version; -- frozen and offline exporter arguments; + member type, member size, executable size, executable mode, and exact version; +- frozen, offline, cacheless, noninteractive exporter arguments; +- isolated environment directories and exclusion of arbitrary ambient variables; +- continued project metadata discovery with no `--no-config` regression; - timeout, process, parse, and exporter failures fail closed; - orphan locks and empty third-party closures remain nonfatal and explicit; -- every nonempty emitted requirement includes a hash; and +- every nonempty line is a normalized exact package pin with one or more complete + SHA-256 hashes; and - the changed production module retains 100% statement and branch coverage and 100% production docstrings. @@ -67,11 +104,15 @@ August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/export/ Astral Software, Inc. (n.d.). *Locking and syncing*. uv documentation. Retrieved August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/sync/ -Supply-chain Levels for Software Artifacts. (2026). *SLSA specification +Astral Software, Inc. (n.d.). *The uv command-line interface*. uv documentation. +Retrieved August 4, 2026, from https://docs.astral.sh/uv/reference/cli/ + +Supply-chain Levels for Software Artifacts. (2025). *SLSA specification (version 1.2)*. https://slsa.dev/spec/v1.2/ -Supply-chain Levels for Software Artifacts. (2026). *Provenance (version 1.2)*. +Supply-chain Levels for Software Artifacts. (2025). *Provenance (version 1.2)*. https://slsa.dev/spec/v1.2/provenance -Supply-chain Levels for Software Artifacts. (2026). *Source: Requirements for -producing source (version 1.2)*. https://slsa.dev/spec/v1.2/source-requirements +Supply-chain Levels for Software Artifacts. (2025). *Source: Requirements for +producing source (version 1.2)*. +https://slsa.dev/spec/v1.2/source-requirements From 15c97c82bce4247b86554d01a08bd7aabfab3b2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:05:59 +0900 Subject: [PATCH 11/20] test(coverage): fail closed on tracked uv metadata read errors --- tests/test_uv_export_isolation_contract.py | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_uv_export_isolation_contract.py b/tests/test_uv_export_isolation_contract.py index 04da0535c..f24a6ac8e 100644 --- a/tests/test_uv_export_isolation_contract.py +++ b/tests/test_uv_export_isolation_contract.py @@ -95,3 +95,33 @@ def test_uv_export_accepts_exact_package_pins_with_markers_and_multiple_hashes() ) assert materializer._is_fully_hash_pinned_export(content) is True + + +def test_tracked_pyproject_read_failure_is_not_misclassified_as_orphan( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A present sibling metadata blob that cannot be read must fail closed.""" + tree = ( + b"100644 blob " + b"a" * 40 + b"\tpyproject.toml\0" + b"100644 blob " + b"b" * 40 + b"\tuv.lock\0" + ) + + def fake_git(_repo_root: Path, *args: str) -> bytes: + if args[0] == "ls-tree": + return tree + if args[0] == "show" and args[1].endswith(":uv.lock"): + return b"version = 1\n" + if args[0] == "show" and args[1].endswith(":pyproject.toml"): + raise RuntimeError("tracked metadata blob could not be read") + raise AssertionError(args) + + monkeypatch.setattr(materializer, "_git", fake_git) + monkeypatch.setattr( + materializer, + "_install_trusted_uv", + lambda: (_ for _ in ()).throw(AssertionError("uv must not start")), + ) + + with pytest.raises(RuntimeError, match="tracked metadata blob could not be read"): + materializer.base_hash_locks(tmp_path, "a" * 40) From 7e66e5a18b7312733b95b0a395882fab9a847fc8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:08:26 +0900 Subject: [PATCH 12/20] fix(coverage): distinguish orphan uv locks from Git read failures --- .../materialize_base_python_requirements.py | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 891b3ce31..ec0f500dd 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -283,30 +283,30 @@ def _run_uv_export( ) +def _uv_pyproject_path(lock_path: str) -> str: + """Return the sibling project metadata path for one safe tracked uv lock.""" + project_dir = pathlib.PurePosixPath(lock_path).parent + return ( + "pyproject.toml" + if str(project_dir) == "." + else f"{project_dir}/pyproject.toml" + ) + + def _export_uv_lock( repo_root: pathlib.Path, base_sha: str, lock_path: str ) -> bytes | None: """Export one tracked base ``uv.lock`` into a trusted hash-pinned closure. - The sibling ``pyproject.toml`` determines whether the lock belongs to an - exportable project. Orphan locks are ignored, and a successful comment-only - export represents a valid project with no third-party dependency closure. - Every other exporter failure is fatal: silently dropping a tracked project - lock would execute coverage without the base dependencies and could turn - import failures into misleading review feedback. + The caller proves that the sibling ``pyproject.toml`` is a regular blob in + the same exact base tree before invoking this function. Any later Git read + failure is therefore an integrity or availability failure, not evidence of + an orphan lock, and propagates fail-closed. A successful comment-only export + represents a valid project with no third-party dependency closure. """ - project_dir = pathlib.PurePosixPath(lock_path).parent - pyproject_path = ( - "pyproject.toml" - if str(project_dir) == "." - else f"{project_dir}/pyproject.toml" - ) + pyproject_path = _uv_pyproject_path(lock_path) lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}") - try: - pyproject_content = _git(repo_root, "show", f"{base_sha}:{pyproject_path}") - except RuntimeError: - return None - + pyproject_content = _git(repo_root, "show", f"{base_sha}:{pyproject_path}") uv_path = _install_trusted_uv() with tempfile.TemporaryDirectory() as work_dir: @@ -343,13 +343,9 @@ def _export_uv_lock( return exported -def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: - """Return regular hash-lock blobs from the exact validated base commit.""" - if not SHA_RE.fullmatch(base_sha): - raise ValueError("base SHA must be exactly 40 hexadecimal characters") - - locks: list[tuple[str, bytes]] = [] - entries = _git(repo_root, "ls-tree", "-r", "-z", "--full-tree", base_sha) +def _regular_base_blob_paths(entries: bytes) -> list[tuple[str, pathlib.PurePosixPath]]: + """Parse exact-tree output into safe regular blob paths in repository order.""" + regular_blobs: list[tuple[str, pathlib.PurePosixPath]] = [] for raw_entry in entries.split(b"\0"): if not raw_entry: continue @@ -371,11 +367,27 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b or ".." in candidate.parts ): continue + regular_blobs.append((path, candidate)) + return regular_blobs + + +def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: + """Return regular hash-lock blobs from the exact validated base commit.""" + if not SHA_RE.fullmatch(base_sha): + raise ValueError("base SHA must be exactly 40 hexadecimal characters") + + entries = _git(repo_root, "ls-tree", "-r", "-z", "--full-tree", base_sha) + regular_blobs = _regular_base_blob_paths(entries) + regular_paths = {path for path, _candidate in regular_blobs} + locks: list[tuple[str, bytes]] = [] + for path, candidate in regular_blobs: if _is_candidate_lock_name(candidate.name): content = _git(repo_root, "show", f"{base_sha}:{path}") if _is_hash_pinned(content): locks.append((path, content)) elif candidate.name == "uv.lock": + if _uv_pyproject_path(path) not in regular_paths: + continue exported = _export_uv_lock(repo_root, base_sha, path) if exported is not None: locks.append((path, exported)) From 2ef7a3661d26aa17636fc562f51531993c21313b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:11:18 +0900 Subject: [PATCH 13/20] docs(doctoring): distinguish orphan metadata from read failure --- .../trusted-uv-lock-materialization.md | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md index fc840838c..0ca01fd38 100644 --- a/docs/doctoring/trusted-uv-lock-materialization.md +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -10,25 +10,29 @@ access during export. The implementation therefore: -1. reads `uv.lock` and its sibling `pyproject.toml` only through `git show` at a - validated 40-character commit SHA; -2. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; -3. verifies the bounded archive with a pinned SHA-256 digest before extraction; -4. accepts only the expected regular-file tar member within explicit size bounds; -5. writes the executable with mode `0755` and verifies that it reports the exact +1. inventories regular blobs from the validated 40-character base commit before + deciding whether a `uv.lock` has a sibling `pyproject.toml`; +2. reads an inventoried lock and project file only through `git show` at that + same immutable revision; an absent sibling is an explicit orphan, while a + read failure for an inventoried blob is fatal and cannot be misclassified as + absence; +3. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; +4. verifies the bounded archive with a pinned SHA-256 digest before extraction; +5. accepts only the expected regular-file tar member within explicit size bounds; +6. writes the executable with mode `0755` and verifies that it reports the exact pinned `uv` version; -6. executes `uv export` with `--frozen`, `--offline`, `--no-cache`, +7. executes `uv export` with `--frozen`, `--offline`, `--no-cache`, `--no-progress`, `--color never`, `--no-emit-project`, and `--no-editable` in an isolated temporary project; -7. supplies a minimal environment with isolated home, temporary, cache, and +8. supplies a minimal environment with isolated home, temporary, cache, and configuration directories, disables dotenv loading and managed Python downloads, and does not inherit arbitrary runner variables; -8. keeps project metadata discovery enabled because the reconstructed +9. keeps project metadata discovery enabled because the reconstructed `pyproject.toml` is an authoritative input; `--no-config` is deliberately not used because uv documents that it disables `pyproject.toml` discovery; -9. rejects every nonempty export unless every logical line is an exact normalized - package `==` pin followed only by complete SHA-256 hashes; and -10. exposes only generated requirements files and a source manifest to the later +10. rejects every nonempty export unless every logical line is an exact normalized + package `==` pin followed only by complete SHA-256 hashes; and +11. exposes only generated requirements files and a source manifest to the later networkless coverage environment. ## Standards and current-tool rationale @@ -63,6 +67,11 @@ be a complete `sha256` digest. Option lines, direct or local references, other algorithms, truncated digests, and global directives are rejected even when they contain a `--hash=` substring. +The download response is required to remain on the HTTPS +`releases.astral.sh` host and the artifact bytes must match the pinned digest. +The digest is the executable payload identity; the host check prevents an +unreviewed cross-origin redirect from becoming the transport source. + ## Modular and workspace boundary Nested standalone services are supported: a repository may contain several @@ -75,17 +84,20 @@ A true uv workspace can require member `pyproject.toml` files in addition to the root lock and root project metadata. The current materializer does not reconstruct arbitrary workspace members. Such an export therefore fails closed instead of silently producing incomplete dependency evidence. Workspace-member -reconstruction must be implemented as a separate bounded change that enumerates -member metadata from the same immutable base tree and proves `--all-packages` -and local-package omission semantics before it is enabled. +reconstruction is tracked separately in `.github#750`; that change must enumerate +member metadata from the same immutable base tree and prove `--all-packages` and +local-package omission semantics before it is enabled. ## Verification contract Regression coverage must prove: - base-revision-only reads and rejection of unsafe revision/path shapes; -- fixed-origin download, redirect rejection, bounded reads, archive digest, - member type, member size, executable size, executable mode, and exact version; +- an absent sibling project is skipped, but an inventoried project blob that + cannot be read propagates a fatal error before uv starts; +- fixed-host download, cross-host redirect rejection, bounded reads, archive + digest, member type, member size, executable size, executable mode, and exact + version; - frozen, offline, cacheless, noninteractive exporter arguments; - isolated environment directories and exclusion of arbitrary ambient variables; - continued project metadata discovery with no `--no-config` regression; From 3ff45c42ff504aabf2cc2418d51db828ac999122 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:24:28 +0900 Subject: [PATCH 14/20] test(security): reject uv redirects before follow --- tests/test_uv_redirect_boundary.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_uv_redirect_boundary.py diff --git a/tests/test_uv_redirect_boundary.py b/tests/test_uv_redirect_boundary.py new file mode 100644 index 000000000..9accdadc1 --- /dev/null +++ b/tests/test_uv_redirect_boundary.py @@ -0,0 +1,60 @@ +"""Behavioral contracts for the trusted uv download redirect boundary.""" + +from __future__ import annotations + +import urllib.request + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +def test_trusted_uv_redirect_handler_rejects_before_following() -> None: + """Every HTTP redirect is rejected before urllib creates a target request.""" + handler = materializer._RejectTrustedUvRedirects() + original = urllib.request.Request(materializer.TRUSTED_UV_ARCHIVE_URL) + + with pytest.raises(RuntimeError, match="redirects are forbidden"): + handler.redirect_request( + original, + None, + 302, + "Found", + {}, + "https://127.0.0.1/internal", + ) + + +def test_trusted_uv_opener_is_cached_and_disables_ambient_proxies( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The dedicated process installs one no-proxy, no-redirect opener.""" + materializer._install_trusted_uv_url_opener.cache_clear() + captured: dict[str, object] = {"builds": 0, "installs": 0} + sentinel = object() + + def fake_build_opener(*handlers: object) -> object: + captured["builds"] = int(captured["builds"]) + 1 + captured["handlers"] = handlers + return sentinel + + def fake_install_opener(opener: object) -> None: + captured["installs"] = int(captured["installs"]) + 1 + captured["opener"] = opener + + monkeypatch.setattr(materializer.urllib.request, "build_opener", fake_build_opener) + monkeypatch.setattr(materializer.urllib.request, "install_opener", fake_install_opener) + + materializer._install_trusted_uv_url_opener() + materializer._install_trusted_uv_url_opener() + + assert captured["builds"] == 1 + assert captured["installs"] == 1 + assert captured["opener"] is sentinel + handlers = captured["handlers"] + assert isinstance(handlers, tuple) + assert len(handlers) == 2 + assert isinstance(handlers[0], urllib.request.ProxyHandler) + assert handlers[0].proxies == {} + assert isinstance(handlers[1], materializer._RejectTrustedUvRedirects) + materializer._install_trusted_uv_url_opener.cache_clear() From 41afe7b64ffd7e5d6a89f3e1786f51b2c8f7f834 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:26:44 +0900 Subject: [PATCH 15/20] test(coverage): enforce uv origin port and branch evidence --- .../test_uv_redirect_and_coverage_contract.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_uv_redirect_and_coverage_contract.py diff --git a/tests/test_uv_redirect_and_coverage_contract.py b/tests/test_uv_redirect_and_coverage_contract.py new file mode 100644 index 000000000..952b479d1 --- /dev/null +++ b/tests/test_uv_redirect_and_coverage_contract.py @@ -0,0 +1,87 @@ +"""Regression contracts for the trusted uv origin and coverage evidence.""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +class _FakeResponse: + """Minimal context-managed response exposing one deterministic final URL.""" + + def __init__(self, final_url: str, payload: bytes = b"archive") -> None: + """Store the final redirect URL and bounded response payload.""" + self._final_url = final_url + self._payload = payload + + def __enter__(self) -> "_FakeResponse": + """Return this response from the context manager.""" + return self + + def __exit__(self, *_args: object) -> None: + """Leave the synthetic response context without suppressing errors.""" + + def geturl(self) -> str: + """Return the URL observed after redirects.""" + return self._final_url + + def read(self, size: int) -> bytes: + """Return at most the requested number of bytes.""" + return self._payload[:size] + + +@pytest.mark.parametrize( + "unsafe_url", + [ + "https://releases.astral.sh:444/github/uv/releases/download/0.12.1/uv.tar.gz", + "https://releases.astral.sh:not-a-port/github/uv/releases/download/0.12.1/uv.tar.gz", + ], +) +def test_trusted_uv_download_rejects_nondefault_or_malformed_ports( + monkeypatch: pytest.MonkeyPatch, + unsafe_url: str, +) -> None: + """The pinned Astral host cannot redirect to another or malformed service port.""" + + response = _FakeResponse(unsafe_url) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_args, **_kwargs: response, + ) + + with pytest.raises(RuntimeError, match="redirected outside"): + materializer._download_trusted_uv_archive() + + +def test_trusted_uv_download_accepts_explicit_default_https_port( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An explicit port 443 still denotes the fixed trusted HTTPS origin.""" + + response = _FakeResponse( + "https://releases.astral.sh:443/github/uv/releases/download/0.12.1/uv.tar.gz" + ) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_args, **_kwargs: response, + ) + + assert materializer._download_trusted_uv_archive() == b"archive" + + +def test_repository_coverage_contract_enforces_branches_at_one_hundred_percent() -> None: + """The declared 100% quality gate measures branch as well as statement coverage.""" + + repository_root = Path(__file__).resolve().parents[1] + configuration = tomllib.loads( + (repository_root / "pyproject.toml").read_text(encoding="utf-8") + ) + + assert configuration["tool"]["coverage"]["run"]["branch"] is True + assert configuration["tool"]["coverage"]["report"]["fail_under"] == 100 From 8ffc9cc82f02cbbdc9b0662fea8e314f8fa2dcb8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:26:55 +0900 Subject: [PATCH 16/20] test(coverage): measure branch coverage at the 100 percent gate --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ee2585bf0..f837b4354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dev = [ pythonpath = ["."] [tool.coverage.run] +branch = true source = ["scripts/ci"] omit = ["tests/*"] From da6157a3fde622516e10f4d827ef6bb7a08130a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:27:04 +0900 Subject: [PATCH 17/20] fix(security): reject trusted uv redirects before follow --- .../materialize_base_python_requirements.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index ec0f500dd..7e3ee5120 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -20,6 +20,7 @@ import tempfile import urllib.parse import urllib.request +from typing import Any SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") @@ -45,6 +46,33 @@ TRUSTED_UV_VERSION_TIMEOUT_SECONDS = 10 +class _RejectTrustedUvRedirects(urllib.request.HTTPRedirectHandler): + """Reject every redirect before urllib issues a request to its target.""" + + def redirect_request( + self, + request: urllib.request.Request, + response: Any, + code: int, + message: str, + headers: Any, + new_url: str, + ) -> None: + """Fail closed for all redirect status codes and target locations.""" + del request, response, code, message, headers, new_url + raise RuntimeError("trusted uv archive redirects are forbidden") + + +@functools.cache +def _install_trusted_uv_url_opener() -> None: + """Install one process-wide no-proxy, no-redirect opener for the fixed URL.""" + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), + _RejectTrustedUvRedirects(), + ) + urllib.request.install_opener(opener) + + def _is_candidate_lock_name(name: str) -> bool: """Return whether a file name is a possible pip requirements lock.""" return name == "requirements.lock" or ( @@ -133,6 +161,7 @@ def _git(repo_root: pathlib.Path, *args: str) -> bytes: def _download_trusted_uv_archive() -> bytes: """Download the fixed uv release archive through one HTTPS trust boundary.""" + _install_trusted_uv_url_opener() try: # Keep the audited URL literal at the network sink so static analysis can # prove that neither user data nor repository content selects a scheme, @@ -148,7 +177,7 @@ def _download_trusted_uv_archive() -> bytes: "releases.astral.sh", ): raise RuntimeError( - "trusted uv archive redirected outside releases.astral.sh" + "trusted uv archive response escaped releases.astral.sh" ) payload = response.read(TRUSTED_UV_DOWNLOAD_MAX_BYTES + 1) except OSError as exc: From 173945917a36b8fce8f382bbee2a98eac339979d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:29:00 +0900 Subject: [PATCH 18/20] docs(doctoring): record no-proxy no-redirect uv transport --- .../trusted-uv-lock-materialization.md | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md index 0ca01fd38..8761dfcee 100644 --- a/docs/doctoring/trusted-uv-lock-materialization.md +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -16,23 +16,25 @@ The implementation therefore: same immutable revision; an absent sibling is an explicit orphan, while a read failure for an inventoried blob is fatal and cannot be misclassified as absence; -3. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; -4. verifies the bounded archive with a pinned SHA-256 digest before extraction; -5. accepts only the expected regular-file tar member within explicit size bounds; -6. writes the executable with mode `0755` and verifies that it reports the exact +3. installs one process-wide urllib opener with an empty proxy map and a redirect + handler that rejects every redirect before urllib creates a target request; +4. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; +5. verifies the bounded archive with a pinned SHA-256 digest before extraction; +6. accepts only the expected regular-file tar member within explicit size bounds; +7. writes the executable with mode `0755` and verifies that it reports the exact pinned `uv` version; -7. executes `uv export` with `--frozen`, `--offline`, `--no-cache`, +8. executes `uv export` with `--frozen`, `--offline`, `--no-cache`, `--no-progress`, `--color never`, `--no-emit-project`, and `--no-editable` in an isolated temporary project; -8. supplies a minimal environment with isolated home, temporary, cache, and +9. supplies a minimal environment with isolated home, temporary, cache, and configuration directories, disables dotenv loading and managed Python downloads, and does not inherit arbitrary runner variables; -9. keeps project metadata discovery enabled because the reconstructed - `pyproject.toml` is an authoritative input; `--no-config` is deliberately not - used because uv documents that it disables `pyproject.toml` discovery; -10. rejects every nonempty export unless every logical line is an exact normalized +10. keeps project metadata discovery enabled because the reconstructed + `pyproject.toml` is an authoritative input; `--no-config` is deliberately not + used because uv documents that it disables `pyproject.toml` discovery; +11. rejects every nonempty export unless every logical line is an exact normalized package `==` pin followed only by complete SHA-256 hashes; and -11. exposes only generated requirements files and a source manifest to the later +12. exposes only generated requirements files and a source manifest to the later networkless coverage environment. ## Standards and current-tool rationale @@ -67,10 +69,12 @@ be a complete `sha256` digest. Option lines, direct or local references, other algorithms, truncated digests, and global directives are rejected even when they contain a `--hash=` substring. -The download response is required to remain on the HTTPS -`releases.astral.sh` host and the artifact bytes must match the pinned digest. -The digest is the executable payload identity; the host check prevents an -unreviewed cross-origin redirect from becoming the transport source. +The download request uses neither ambient proxy configuration nor automatic +redirect following. Any HTTP redirect is rejected before a request to the target +location can be created. The fixed HTTPS origin is still verified on the +response as defense in depth, and the archive bytes must match the pinned digest. +The redirect boundary prevents unintended network side effects; the digest pin +separately establishes executable payload identity. ## Modular and workspace boundary @@ -95,9 +99,10 @@ Regression coverage must prove: - base-revision-only reads and rejection of unsafe revision/path shapes; - an absent sibling project is skipped, but an inventoried project blob that cannot be read propagates a fatal error before uv starts; -- fixed-host download, cross-host redirect rejection, bounded reads, archive - digest, member type, member size, executable size, executable mode, and exact - version; +- the download opener is cached, disables ambient proxies, and rejects redirects + before following them; +- fixed-host response validation, bounded reads, archive digest, member type, + member size, executable size, executable mode, and exact version; - frozen, offline, cacheless, noninteractive exporter arguments; - isolated environment directories and exclusion of arbitrary ambient variables; - continued project metadata discovery with no `--no-config` regression; From 00642c24a7dde0fa50ca425c33311dd853da011c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:30:32 +0900 Subject: [PATCH 19/20] fix(security): enforce fixed Astral HTTPS origin port --- .../ci/materialize_base_python_requirements.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 7e3ee5120..53b4788dd 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -172,12 +172,21 @@ def _download_trusted_uv_archive() -> bytes: timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS, ) as response: final_url = urllib.parse.urlparse(response.geturl()) - if (final_url.scheme, final_url.hostname) != ( - "https", - "releases.astral.sh", + try: + final_port = final_url.port + except ValueError as exc: + raise RuntimeError( + "trusted uv archive redirected outside the fixed " + "releases.astral.sh HTTPS origin" + ) from exc + if ( + (final_url.scheme, final_url.hostname) + != ("https", "releases.astral.sh") + or final_port not in (None, 443) ): raise RuntimeError( - "trusted uv archive response escaped releases.astral.sh" + "trusted uv archive redirected outside the fixed " + "releases.astral.sh HTTPS origin" ) payload = response.read(TRUSTED_UV_DOWNLOAD_MAX_BYTES + 1) except OSError as exc: From 3a15594b9a5085fcbac002471c8f532ac130e40f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 21:31:33 +0900 Subject: [PATCH 20/20] docs(doctoring): record fixed origin port and branch coverage evidence --- .../trusted-uv-lock-materialization.md | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md index 8761dfcee..79e9d98c8 100644 --- a/docs/doctoring/trusted-uv-lock-materialization.md +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -18,7 +18,10 @@ The implementation therefore: absence; 3. installs one process-wide urllib opener with an empty proxy map and a redirect handler that rejects every redirect before urllib creates a target request; -4. downloads one fixed official Astral `uv` archive from a literal HTTPS URL; +4. downloads one fixed official Astral `uv` archive from a literal HTTPS URL and + accepts a response only when its parsed origin remains HTTPS, + `releases.astral.sh`, and the absent or explicit default port 443; malformed + or nondefault ports fail closed; 5. verifies the bounded archive with a pinned SHA-256 digest before extraction; 6. accepts only the expected regular-file tar member within explicit size bounds; 7. writes the executable with mode `0755` and verifies that it reports the exact @@ -71,10 +74,12 @@ contain a `--hash=` substring. The download request uses neither ambient proxy configuration nor automatic redirect following. Any HTTP redirect is rejected before a request to the target -location can be created. The fixed HTTPS origin is still verified on the -response as defense in depth, and the archive bytes must match the pinned digest. -The redirect boundary prevents unintended network side effects; the digest pin -separately establishes executable payload identity. +location can be created. The parsed response origin is still checked as defense +in depth: a nondefault or malformed port is a distinct authority and cannot be +accepted merely because the scheme and hostname match. The archive bytes must +then match the pinned digest. The redirect and origin boundaries prevent +unintended network side effects; the digest pin separately establishes +executable payload identity. ## Modular and workspace boundary @@ -101,8 +106,10 @@ Regression coverage must prove: cannot be read propagates a fatal error before uv starts; - the download opener is cached, disables ambient proxies, and rejects redirects before following them; -- fixed-host response validation, bounded reads, archive digest, member type, - member size, executable size, executable mode, and exact version; +- fixed HTTPS scheme and hostname validation, acceptance only of an absent or + explicit port 443, rejection of malformed and nondefault ports, bounded reads, + archive digest, member type, member size, executable size, executable mode, + and exact version; - frozen, offline, cacheless, noninteractive exporter arguments; - isolated environment directories and exclusion of arbitrary ambient variables; - continued project metadata discovery with no `--no-config` regression; @@ -110,8 +117,8 @@ Regression coverage must prove: - orphan locks and empty third-party closures remain nonfatal and explicit; - every nonempty line is a normalized exact package pin with one or more complete SHA-256 hashes; and -- the changed production module retains 100% statement and branch coverage and - 100% production docstrings. +- `pyproject.toml` enables branch measurement and the changed production module + retains 100% statement and branch coverage plus 100% production docstrings. ## References @@ -124,6 +131,10 @@ August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/sync/ Astral Software, Inc. (n.d.). *The uv command-line interface*. uv documentation. Retrieved August 4, 2026, from https://docs.astral.sh/uv/reference/cli/ +Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform Resource Identifier +(URI): Generic syntax* (STD 66; RFC 3986). Internet Engineering Task Force. +https://doi.org/10.17487/RFC3986 + Supply-chain Levels for Software Artifacts. (2025). *SLSA specification (version 1.2)*. https://slsa.dev/spec/v1.2/