From 76de2cb5db8bd864433cd3d6655650d5943df743 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 25 Sep 2026 22:54:28 +0100 Subject: [PATCH] fix(release): identify the R2 manifest validator to Cloudflare Bot management on downloads.terraphim.ai answers the default Python-urllib User-Agent with 403, which has been failing the hourly r2-manifest-health workflow and local operator runs even though every channel object is healthy. Both fetch paths now send terraphim-r2-manifest-validator/1.0. Verified locally: all three binaries validate legacy=strict=1.21.16 with full size and SHA-256 checks. Refs #337 --- scripts/validate-r2-manifests.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/validate-r2-manifests.py b/scripts/validate-r2-manifests.py index 9bf0ee4..67188b0 100755 --- a/scripts/validate-r2-manifests.py +++ b/scripts/validate-r2-manifests.py @@ -46,9 +46,13 @@ def version_tuple(value: str) -> tuple[int, int, int]: def fetch(base_url: str, path: str, limit: int) -> bytes: - with urllib.request.urlopen( # nosec B310: validate() allowlists the scheme - f"{base_url}/{path}", timeout=60 - ) as response: + # Cloudflare bot management on the public channel answers the default + # Python-urllib User-Agent with 403, so identify as the validator. + request = urllib.request.Request( # nosec B310: validate() allowlists the scheme + f"{base_url}/{path}", + headers={"User-Agent": "terraphim-r2-manifest-validator/1.0"}, + ) + with urllib.request.urlopen(request, timeout=60) as response: data = response.read(limit + 1) if len(data) > limit: raise ValueError(f"{path}: response exceeds {limit} bytes") @@ -144,9 +148,11 @@ def verify_asset( raise ValueError("chunk size must be positive") digest = hashlib.sha256() total = 0 - with opener( # nosec B310: validate() allowlists the scheme - f"{base_url}/{path}", timeout=60 - ) as response: + request = urllib.request.Request( # nosec B310: validate() allowlists the scheme + f"{base_url}/{path}", + headers={"User-Agent": "terraphim-r2-manifest-validator/1.0"}, + ) + with opener(request, timeout=60) as response: while True: chunk = response.read(min(chunk_size, declared_size - total + 1)) if not chunk: