From ccfe7c69e1368b4edeb420d878f29eee4bf245cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 13:17:24 +0900 Subject: [PATCH 1/5] fix(coverage): set a stable User-Agent for trusted uv bootstrap --- scripts/ci/materialize_base_python_requirements.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 98cdad459..ec72253a1 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -38,6 +38,8 @@ 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" +# The release host rejects Python's default User-Agent with HTTP 403. +TRUSTED_UV_USER_AGENT = "ContextualWisdomLab-OpenCode-Coverage/1" TRUSTED_UV_ARCHIVE_URL = ( "https://releases.astral.sh/github/uv/releases/download/0.12.1/" "uv-x86_64-unknown-linux-gnu.tar.gz" @@ -172,9 +174,13 @@ def _download_trusted_uv_archive() -> bytes: # 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( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310 + request = urllib.request.Request( "https://releases.astral.sh/github/uv/releases/download/0.12.1/" "uv-x86_64-unknown-linux-gnu.tar.gz", + headers={"User-Agent": TRUSTED_UV_USER_AGENT}, + ) + with urllib.request.urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310 + request, timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS, ) as response: final_url = urllib.parse.urlparse(response.geturl()) From 233df92764d449ff4c02064e342a9f2b81a514b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 13:17:52 +0900 Subject: [PATCH 2/5] test(coverage): lock the trusted uv bootstrap User-Agent --- tests/test_materialize_base_python_requirements.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 8a383f0c2..5dfdd7dde 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -503,12 +503,22 @@ def _trusted_uv_archive( 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.""" + """The downloader sends a stable User-Agent to the fixed Astral HTTPS origin.""" payload = b"archive" response = FakeHttpResponse(materializer.TRUSTED_UV_ARCHIVE_URL, payload) - monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + captured: dict[str, object] = {} + + def fake_urlopen(request: object, **_kwargs: object) -> FakeHttpResponse: + captured["request"] = request + return response + + monkeypatch.setattr(materializer.urllib.request, "urlopen", fake_urlopen) assert materializer._download_trusted_uv_archive() == payload + request = captured["request"] + assert isinstance(request, materializer.urllib.request.Request) + assert request.full_url == materializer.TRUSTED_UV_ARCHIVE_URL + assert request.get_header("User-agent") == materializer.TRUSTED_UV_USER_AGENT def test_download_trusted_uv_archive_rejects_unsafe_redirect( From 4b7dde4d7fd8335fd4860d1c3eb6364018266da7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 13:24:38 +0900 Subject: [PATCH 3/5] docs(coverage): record trusted uv User-Agent boundary --- .../trusted-uv-lock-materialization.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md index 8f78759ca..906d3b214 100644 --- a/docs/doctoring/trusted-uv-lock-materialization.md +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -38,7 +38,10 @@ The implementation therefore: 11. rejects every nonempty export unless every logical line is an exact normalized package `==` pin followed only by complete SHA-256 hashes; and 12. exposes only generated requirements files and a source manifest to the later - networkless coverage environment. + networkless coverage environment; +13. sends a fixed repository-owned `User-Agent` value on the archive request so + the release host does not reject Python's default `urllib` identity, without + allowing that header to select the URL, origin, proxy, redirect, or payload. ## Standards and current-tool rationale @@ -72,6 +75,13 @@ 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 request sends a fixed repository-owned `User-Agent` value. This is +request metadata only: it does not change the literal URL, fixed HTTPS origin, +empty proxy map, redirect handler, byte bound, or archive digest. The explicit +value is required because the release host rejects Python's default +`urllib` identity with HTTP 403 even though the same fixed URL is available to +an explicit client identity. + 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 parsed response origin is still checked as defense @@ -104,8 +114,8 @@ 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; -- the download opener is cached, disables ambient proxies, and rejects redirects - before following them; +- the download opener is cached, uses the fixed repository-owned `User-Agent`, + disables ambient proxies, and rejects redirects before following them; - 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, @@ -171,6 +181,9 @@ accepted by the coverage sandbox. ## References +Internet Engineering Task Force. (2022). *HTTP semantics* (RFC 9110), +Section 10.1.5, User-Agent. https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5 + Astral Software, Inc. (n.d.). *Exporting a lockfile*. uv documentation. Retrieved August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/export/ From 601b254f3a8ea4cc593e7089d6baeadd9d8d3ee4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 13:30:02 +0900 Subject: [PATCH 4/5] test(coverage): permit only a static trusted uv request --- tests/test_trusted_uv_download_contract.py | 40 +++++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/test_trusted_uv_download_contract.py b/tests/test_trusted_uv_download_contract.py index 02f3c5961..da5a8780c 100644 --- a/tests/test_trusted_uv_download_contract.py +++ b/tests/test_trusted_uv_download_contract.py @@ -12,6 +12,7 @@ "https://releases.astral.sh/github/uv/releases/download/0.12.1/" "uv-x86_64-unknown-linux-gnu.tar.gz" ) +_EXPECTED_USER_AGENT = "ContextualWisdomLab-OpenCode-Coverage/1" _SEMGREP_DYNAMIC_URL_RULE = ( "python.lang.security.audit.dynamic-urllib-use-detected." "dynamic-urllib-use-detected" @@ -53,16 +54,15 @@ def _urlopen_calls() -> list[ast.Call]: ] -def test_urlopen_receives_one_literal_https_release_url() -> None: - """Static analysis can prove repository or user data never selects the URL.""" +def test_urlopen_receives_one_static_request() -> None: + """Static analysis can prove the downloader passes one named request object.""" calls = _urlopen_calls() 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 + request_argument = calls[0].args[0] + assert isinstance(request_argument, ast.Name) + assert request_argument.id == "request" def test_literal_network_sink_matches_the_documented_release_constant() -> None: @@ -70,8 +70,13 @@ def test_literal_network_sink_matches_the_documented_release_constant() -> None: 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``.""" +def test_literal_user_agent_matches_the_documented_identity() -> None: + """The request identity remains fixed and contains no user-controlled data.""" + assert _assigned_literal("TRUSTED_UV_USER_AGENT") == _EXPECTED_USER_AGENT + + +def test_downloader_constructs_one_static_request() -> None: + """The request URL and User-Agent are both statically constrained.""" request_calls = [ node for node in ast.walk(_download_function()) @@ -80,7 +85,24 @@ def test_downloader_never_constructs_a_dynamic_request_object() -> None: and node.func.attr == "Request" ] - assert request_calls == [] + assert len(request_calls) == 1 + request_call = request_calls[0] + assert len(request_call.args) == 1 + url_argument = request_call.args[0] + assert isinstance(url_argument, ast.Constant) + assert url_argument.value == _EXPECTED_URL + + headers_keyword = next( + keyword for keyword in request_call.keywords if keyword.arg == "headers" + ) + assert isinstance(headers_keyword.value, ast.Dict) + assert len(headers_keyword.value.keys) == 1 + key = headers_keyword.value.keys[0] + value = headers_keyword.value.values[0] + assert isinstance(key, ast.Constant) + assert key.value == "User-Agent" + assert isinstance(value, ast.Name) + assert value.id == "TRUSTED_UV_USER_AGENT" def test_literal_urlopen_sink_has_one_scoped_semgrep_suppression() -> None: From aae09fd95d9c5df7c72b8a19bbcf0a842810bbaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 19:36:30 +0900 Subject: [PATCH 5/5] docs(uv): distinguish request User-Agent from opener --- docs/doctoring/trusted-uv-lock-materialization.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md index 906d3b214..a1866654f 100644 --- a/docs/doctoring/trusted-uv-lock-materialization.md +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -114,8 +114,9 @@ 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; -- the download opener is cached, uses the fixed repository-owned `User-Agent`, - disables ambient proxies, and rejects redirects before following them; +- the download opener is cached, disables ambient proxies, and rejects redirects + before following them; the download request uses the fixed repository-owned + `User-Agent`; - 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,