From 059fef1cacbda905c00502c006115e5a362bf493 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 26 Jul 2026 09:28:33 +0000 Subject: [PATCH] build: freeze operating system package inputs --- .gitea/workflows/refresh-image-locks.yml | 3 + Dockerfile.gateway | 12 +++- Dockerfile.orchestrator.fc | 10 ++- bot_bottle/contrib/claude/Dockerfile | 12 +++- bot_bottle/contrib/codex/Dockerfile | 12 +++- bot_bottle/contrib/pi/Dockerfile | 12 +++- bot_bottle/contrib/pi/package-lock.json | 9 ++- scripts/complete_npm_lock_integrity.py | 65 ++++++++++++++++++ .../unit/test_complete_npm_lock_integrity.py | 66 +++++++++++++++++++ 9 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 scripts/complete_npm_lock_integrity.py create mode 100644 tests/unit/test_complete_npm_lock_integrity.py diff --git a/.gitea/workflows/refresh-image-locks.yml b/.gitea/workflows/refresh-image-locks.yml index 847b5bc8..5402ad30 100644 --- a/.gitea/workflows/refresh-image-locks.yml +++ b/.gitea/workflows/refresh-image-locks.yml @@ -41,6 +41,9 @@ jobs: npm install --package-lock-only --ignore-scripts --no-audit --no-fund ) done + python3 scripts/complete_npm_lock_integrity.py \ + bot_bottle/contrib/claude/package-lock.json \ + bot_bottle/contrib/pi/package-lock.json - name: Refresh pinned Codex installer checksum run: | diff --git a/Dockerfile.gateway b/Dockerfile.gateway index dc1317eb..d0b7eead 100644 --- a/Dockerfile.gateway +++ b/Dockerfile.gateway @@ -43,13 +43,23 @@ # is pip-installed to the same effect as the upstream image. FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de +# Freeze apt's package universe as well as the base filesystem. Without a +# snapshot, the same Dockerfile resolves different package versions over time. +ARG DEBIAN_SNAPSHOT=20260724T000000Z +RUN sed -i \ + -e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + /etc/apt/sources.list.d/debian.sources + # Runtime system deps: # git supplies the `git daemon` subcommand (no separate package) # plus the core `git` binary the pre-receive hook invokes. # openssh-client supplies the upstream SSH transport the # pre-receive hook uses to forward accepted refs. # ca-certificates is needed for mitmdump upstream TLS. -RUN apt-get update \ +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends \ git openssh-client ca-certificates \ && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.orchestrator.fc b/Dockerfile.orchestrator.fc index 8221e560..fb38badc 100644 --- a/Dockerfile.orchestrator.fc +++ b/Dockerfile.orchestrator.fc @@ -18,7 +18,15 @@ ARG ORCHESTRATOR_BASE_IMAGE FROM ${ORCHESTRATOR_BASE_IMAGE} -RUN apt-get update \ +ARG DEBIAN_SNAPSHOT=20260724T000000Z +RUN sed -i \ + -e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + /etc/apt/sources.list.d/debian.sources + +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends \ buildah crun netavark aardvark-dns \ && rm -rf /var/lib/apt/lists/* diff --git a/bot_bottle/contrib/claude/Dockerfile b/bot_bottle/contrib/claude/Dockerfile index e5e04621..36da01a4 100644 --- a/bot_bottle/contrib/claude/Dockerfile +++ b/bot_bottle/contrib/claude/Dockerfile @@ -11,6 +11,14 @@ # Version-qualified Node LTS, pinned to its multi-architecture manifest. FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba +ARG DEBIAN_SNAPSHOT=20260724T000000Z +RUN sed -i \ + -e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + /etc/apt/sources.list.d/debian.sources + # Install runtime system deps. claude-code shells out to git for several # features (status checks, commits, PR creation) — without git in the # image, those features fail in surprising ways once the user does any @@ -19,7 +27,7 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e8 # HTTPS_PROXY-aware tool (curl itself, plus anything that shells out # to it) works against egress's bumped TLS without the agent needing # local DNS. -RUN apt-get update \ +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends \ git \ ca-certificates \ @@ -34,7 +42,7 @@ RUN apt-get update \ # (claude-code is a Node CLI), but is convenient for the agent to # shell out to for ad-hoc scripts. Kept on its own layer so it can # be moved to a downstream image if the base ever needs to shrink. -RUN apt-get update \ +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index b876dfbe..373a687e 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -10,7 +10,15 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e8 # installer source and checksum. ARG CODEX_VERSION=0.145.0 -RUN apt-get update \ +ARG DEBIAN_SNAPSHOT=20260724T000000Z +RUN sed -i \ + -e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + /etc/apt/sources.list.d/debian.sources + +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends \ git \ ca-certificates \ @@ -24,7 +32,7 @@ RUN apt-get update \ # (codex is a Node CLI), but is convenient for the agent to shell # out to for ad-hoc scripts. Kept on its own layer so it can be # moved to a downstream image if the base ever needs to shrink. -RUN apt-get update \ +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* diff --git a/bot_bottle/contrib/pi/Dockerfile b/bot_bottle/contrib/pi/Dockerfile index bb2fd92c..5da73c8a 100644 --- a/bot_bottle/contrib/pi/Dockerfile +++ b/bot_bottle/contrib/pi/Dockerfile @@ -4,7 +4,15 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba -RUN apt-get update \ +ARG DEBIAN_SNAPSHOT=20260724T000000Z +RUN sed -i \ + -e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \ + -e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + -e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \ + /etc/apt/sources.list.d/debian.sources + +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends \ git \ ca-certificates \ @@ -15,7 +23,7 @@ RUN apt-get update \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \ && rm -rf /var/lib/apt/lists/* -RUN apt-get update \ +RUN apt-get -o Acquire::Check-Valid-Until=false update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* diff --git a/bot_bottle/contrib/pi/package-lock.json b/bot_bottle/contrib/pi/package-lock.json index ecc6c915..69e265ef 100644 --- a/bot_bottle/contrib/pi/package-lock.json +++ b/bot_bottle/contrib/pi/package-lock.json @@ -1056,7 +1056,8 @@ }, "engines": { "node": ">=22.19.0" - } + }, + "integrity": "sha512-yqbh68CyhqxMov/jUogFJfMqlu2Gd37GAki+tr59YCmAPHfomiCA5ESzusXtpGzABeiZFC/OrRdQ4GwCCOMIHA==" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { "version": "0.81.1", @@ -1080,7 +1081,8 @@ }, "engines": { "node": ">=22.19.0" - } + }, + "integrity": "sha512-hzHE7Z8l5mgJk+ke67Lge0rwS2+wbKJrFKl9o5M1R1rh33+cCT7D1AHz1OAtX5wFs90E1/BTGhyJRTUHaMxGvQ==" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { "version": "0.81.1", @@ -1092,7 +1094,8 @@ }, "engines": { "node": ">=22.19.0" - } + }, + "integrity": "sha512-OMEe+Zt8oQYi/rCq3upxsTlIScWL0FPhXwQus34TbQb3EmTx88S7Uzx32JxvQiEeWOw8eDCdJf2PBUBE9r6wIg==" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { "version": "1.52.0", diff --git a/scripts/complete_npm_lock_integrity.py b/scripts/complete_npm_lock_integrity.py new file mode 100644 index 00000000..46dd1e0b --- /dev/null +++ b/scripts/complete_npm_lock_integrity.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Complete duplicate npm lock entries from an identical verified artifact.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + + +def complete_lock(path: Path) -> int: + """Fill missing SRI only from the same resolved URL; return change count.""" + data = json.loads(path.read_text(encoding="utf-8")) + packages = data.get("packages", {}) + integrity_by_url: dict[str, str] = {} + for package in packages.values(): + resolved = package.get("resolved") + integrity = package.get("integrity") + if not isinstance(resolved, str) or not resolved.startswith( + ("http://", "https://"), + ): + continue + if not isinstance(integrity, str) or not integrity.startswith("sha512-"): + continue + prior = integrity_by_url.setdefault(resolved, integrity) + if prior != integrity: + raise ValueError(f"conflicting integrity values for {resolved}") + + changed = 0 + missing: list[str] = [] + for package_path, package in packages.items(): + resolved = package.get("resolved") + if not isinstance(resolved, str) or not resolved.startswith( + ("http://", "https://"), + ): + continue + integrity = package.get("integrity") + if isinstance(integrity, str) and integrity.startswith("sha512-"): + continue + known = integrity_by_url.get(resolved) + if known is None: + missing.append(package_path) + continue + package["integrity"] = known + changed += 1 + if missing: + joined = ", ".join(missing) + raise ValueError(f"no verified duplicate artifact for: {joined}") + if changed: + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return changed + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("locks", nargs="+", type=Path) + args = parser.parse_args() + for path in args.locks: + changed = complete_lock(path) + print(f"{path}: completed {changed} duplicate integrity entries") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_complete_npm_lock_integrity.py b/tests/unit/test_complete_npm_lock_integrity.py new file mode 100644 index 00000000..1279b6f9 --- /dev/null +++ b/tests/unit/test_complete_npm_lock_integrity.py @@ -0,0 +1,66 @@ +"""Unit tests for npm's duplicate lock-entry integrity completion.""" + +from __future__ import annotations + +import json +import tempfile +import unittest +from pathlib import Path + +from scripts.complete_npm_lock_integrity import complete_lock + + +class TestCompleteLock(unittest.TestCase): + def _lock(self, directory: str, packages: dict[str, dict[str, str]]) -> Path: + path = Path(directory) / "package-lock.json" + path.write_text(json.dumps({ + "lockfileVersion": 3, + "packages": packages, + })) + return path + + def test_copies_integrity_only_for_identical_resolved_url(self): + url = "https://registry.npmjs.org/example/-/example-1.2.3.tgz" + integrity = "sha512-trusted" + with tempfile.TemporaryDirectory() as directory: + path = self._lock(directory, { + "node_modules/example": { + "resolved": url, + "integrity": integrity, + }, + "node_modules/parent/node_modules/example": { + "resolved": url, + }, + }) + self.assertEqual(1, complete_lock(path)) + packages = json.loads(path.read_text())["packages"] + self.assertEqual( + integrity, + packages["node_modules/parent/node_modules/example"]["integrity"], + ) + + def test_rejects_missing_integrity_without_verified_duplicate(self): + with tempfile.TemporaryDirectory() as directory: + path = self._lock(directory, { + "node_modules/example": { + "resolved": ( + "https://registry.npmjs.org/example/-/example-1.2.3.tgz" + ), + }, + }) + with self.assertRaisesRegex(ValueError, "no verified duplicate"): + complete_lock(path) + + def test_rejects_conflicting_integrities_for_same_url(self): + url = "https://registry.npmjs.org/example/-/example-1.2.3.tgz" + with tempfile.TemporaryDirectory() as directory: + path = self._lock(directory, { + "node_modules/a": {"resolved": url, "integrity": "sha512-a"}, + "node_modules/b": {"resolved": url, "integrity": "sha512-b"}, + }) + with self.assertRaisesRegex(ValueError, "conflicting integrity"): + complete_lock(path) + + +if __name__ == "__main__": + unittest.main()