From 6cd8785487c78d9d54cd843704024eaa9d5cc3a2 Mon Sep 17 00:00:00 2001 From: itswl Date: Sat, 14 Nov 2026 00:08:02 +0800 Subject: [PATCH] python: the services run 3.15, with the locks and CI that have to move with them Dependabot's three base-image bumps (#56 #57 #58) failed as authored: the Python-pin check refused an image on 3.15 tested on 3.14, and the image build could not compile the locked pydantic-core (no 3.15 wheel, no compiler in slim). Moved as one change instead: - the three Dockerfiles, each service's CI pin and the release gate matrix, and the stdlib helper containers in the compose files, so the project has one interpreter; - the locks: pydantic 2.14.3 (pydantic-core 2.50.4), httptools 0.9.0, uvloop 0.23.0 for their 3.15 wheels, and pip-api 0.0.35, which the gate needed: pip-audit's pip-api imported sre_constants, gone in 3.15; - the local venvs, rebuilt on 3.15.2. PyYAML has no 3.15 wheel anywhere; on python:3.15-slim it installs as pure Python, which only the startup config load uses. Gate green on 3.15 at the same test counts; the investigator image's offline engine check unchanged. --- ...2026-11-13-the-services-run-python-3-15.md | 59 +++++++++++++++++++ .github/workflows/ci-hookjudge.yml | 4 +- .github/workflows/ci-hookprobe.yml | 4 +- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 6 +- deploy/docker-compose.work.yml | 6 +- deploy/sidecars/common.py | 2 +- docker-compose.yml | 4 +- hookjudge/Dockerfile | 2 +- hookjudge/requirements.lock | 10 ++-- hookprobe/Dockerfile | 2 +- hookprobe/deploy/docker-compose.prod.yml | 2 +- hookprobe/requirements.lock | 10 ++-- hookrelay/Dockerfile | 2 +- hookrelay/requirements.lock | 10 ++-- 15 files changed, 93 insertions(+), 34 deletions(-) create mode 100644 .agents/notes/implemented/2026-11-13-the-services-run-python-3-15.md diff --git a/.agents/notes/implemented/2026-11-13-the-services-run-python-3-15.md b/.agents/notes/implemented/2026-11-13-the-services-run-python-3-15.md new file mode 100644 index 00000000..fd7c0f40 --- /dev/null +++ b/.agents/notes/implemented/2026-11-13-the-services-run-python-3-15.md @@ -0,0 +1,59 @@ +--- +title: The services run Python 3.15, moved as one change with the locks and CI +status: implemented +date: 2026-11-13 +scope: stack +--- + +## Decision + +The pipe, the judge and the investigator move from `python:3.14-slim` to +`python:3.15-slim`, together with the four things that have to move with +them: + +- each service's CI pin and the release gate matrix; +- the stdlib helper containers in the compose files, so the project has one + interpreter: the demo's sink and stub model, the work stack's sidecars, the + production compose's; +- the locks, for four packages that ship no 3.15 wheel at the locked version + (pydantic 2.13.4 → 2.14.3 for pydantic-core 2.46.4 → 2.50.4, httptools + 0.8.0 → 0.9.0, uvloop 0.22.1 → 0.23.0), and pip-api 0.0.34 → 0.0.35; +- the local venvs the gate runs, rebuilt on 3.15. + +Dependabot's three base-image PRs (#56–#58) are closed in favour of this. + +## Why + +Dependabot proposed the bump as three one-line `FROM` changes, and each +failed two ways (2026-11-13): + +- `assert_python_pins` refused an image on 3.15 tested by CI on 3.14. That is + the check doing its job: the bump has to move the CI pins in the same change. +- The image build failed compiling pydantic-core. The locked version has no + 3.15 wheel, and the slim image has no compiler or Rust toolchain. + +The 3.15 wheels exist one release up for pydantic-core, httptools and uvloop. +PyYAML has no 3.15 wheel in any release, but on `python:3.15-slim` its sdist +installs as pure Python (no C loader), measured. The services load YAML only +at startup, for config, so the slower loader costs nothing that matters. + +The gate on 3.15 found one real incompatibility the image build never would +have, because the build installs no dev tools: `pip-audit` imports pip-api, +whose vendored pyparsing imported `sre_constants`, removed in 3.15. pip-api +0.0.35 (2026-09-08, its issue #294) fixes it. + +## Consequences + +- Measured before the PR, on 3.15.2: + - the full gate green, every suite at its 3.14 count (hookrelay 365, + hookjudge 196, hookprobe 953), and the pipe and judge images built; + - the investigator's image built and the offline engine check unchanged: the + turn completes, an allowed read runs, a protected write is refused and + never runs, and an `@path` is not inlined. +- The published images stay on 3.14 until the next release; nothing deployed + changes before then. +- PyYAML stays a source build until it ships a 3.15 wheel. A future PyYAML + that needs a compiler to install at all would fail the image build, loudly. +- A developer's venv on 3.14 no longer matches what ships. The gate's own + instructions (`python3 -m venv .venv`) pick up whatever `python3` is, so + build venvs with `python3.15` explicitly. diff --git a/.github/workflows/ci-hookjudge.yml b/.github/workflows/ci-hookjudge.yml index fdbac09f..ce2a69af 100644 --- a/.github/workflows/ci-hookjudge.yml +++ b/.github/workflows/ci-hookjudge.yml @@ -31,10 +31,10 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: - # The version this service SHIPS (Dockerfile: python:3.14-slim). It + # The version this service SHIPS (Dockerfile: python:3.15-slim). It # tested on 3.12 while production ran 3.14, so the suite never once # ran on the interpreter that serves traffic. - python-version: "3.14" + python-version: "3.15" cache: "pip" - run: python -m pip install --upgrade pip - run: pip install -r requirements.txt diff --git a/.github/workflows/ci-hookprobe.yml b/.github/workflows/ci-hookprobe.yml index 142b6c2c..5e84cca4 100644 --- a/.github/workflows/ci-hookprobe.yml +++ b/.github/workflows/ci-hookprobe.yml @@ -34,12 +34,12 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: - # The version this service SHIPS (Dockerfile: python:3.14-slim). + # The version this service SHIPS (Dockerfile: python:3.15-slim). # The image also carries the Node runtime and the Claude Code CLI # and moved to 3.14 later than its siblings; this line then kept # saying 3.12, beside a comment claiming to test what ships, until # scripts/assert_python_pins.py started reading both files. - python-version: "3.14" + python-version: "3.15" cache: "pip" - run: python -m pip install --upgrade pip - run: pip install -r requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8d32ebd..e293a7ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,10 +45,10 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: - # The version this service SHIPS (Dockerfile: python:3.14-slim). It + # The version this service SHIPS (Dockerfile: python:3.15-slim). It # tested on 3.12 while production ran 3.14, so the suite never once # ran on the interpreter that serves traffic. - python-version: "3.14" + python-version: "3.15" cache: "pip" - run: python -m pip install --upgrade pip # No ruff pin here: requirements.txt carries it. Naming the version in two diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 913a1bde..00f7be8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,11 +54,11 @@ jobs: # drift. hookprobe's entry said 3.12 while its image was 3.14. include: - service: hookrelay - python: "3.14" + python: "3.15" - service: hookjudge - python: "3.14" + python: "3.15" - service: hookprobe - python: "3.14" + python: "3.15" defaults: run: working-directory: ${{ matrix.service }} diff --git a/deploy/docker-compose.work.yml b/deploy/docker-compose.work.yml index e6fb7925..dbb63bb5 100644 --- a/deploy/docker-compose.work.yml +++ b/deploy/docker-compose.work.yml @@ -196,7 +196,7 @@ services: # before this was written. A byte pump, never an HTTP proxy: the consoles # stream events. doors: - image: python:3.14-slim + image: python:3.15-slim container_name: work-doors restart: unless-stopped command: ["python3", "-u", "/app/doors.py"] @@ -262,7 +262,7 @@ services: - "127.0.0.1:8090:8090" watch-signer: - image: python:3.14-slim + image: python:3.15-slim container_name: work-watch-signer restart: unless-stopped command: ["python3", "-u", "/app/signer.py"] @@ -1135,7 +1135,7 @@ services: # (see `networks:` at the foot of this file), and this has to be reachable # from there AND from the host. networks: [default, probe_net] - image: python:3.14-slim + image: python:3.15-slim container_name: work-sink command: ["python", "-u", "/examples/sink.py"] volumes: diff --git a/deploy/sidecars/common.py b/deploy/sidecars/common.py index 05a1f37c..12f7ac4d 100644 --- a/deploy/sidecars/common.py +++ b/deploy/sidecars/common.py @@ -1,7 +1,7 @@ """What the four sidecars share, in one file beside them. The egress proxy, the MCP gate, the watch signer and the console ingress are -single-file containers: `python:3.14-slim` with a script bind-mounted and no +single-file containers: `python:3.15-slim` with a script bind-mounted and no image to build, which is what lets each of them be read in one sitting and edited without a rebuild. Until 2026-10-06 the plumbing they had in common was COPIED into each file and pinned identical by scripts/assert_copies.py — five diff --git a/docker-compose.yml b/docker-compose.yml index fb4e0268..eb851081 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -137,7 +137,7 @@ services: # an operator would actually have received. Lays out Feishu cards; prints # anything else as JSON. Answers {"code": 0} so the ledger records `sent`. sink: - image: python:3.14-slim + image: python:3.15-slim container_name: hs-sink command: ["python", "-u", "/examples/sink.py"] volumes: @@ -149,7 +149,7 @@ services: # is not a model: it reports itself as `stub-4o-mini`. Put real credentials # in .env and it stops being used. stub-ai: - image: python:3.14-slim + image: python:3.15-slim container_name: hs-stub-ai command: ["python", "-u", "/examples/stub_ai.py"] volumes: diff --git a/hookjudge/Dockerfile b/hookjudge/Dockerfile index 7ce3c677..3d63ac39 100644 --- a/hookjudge/Dockerfile +++ b/hookjudge/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim +FROM python:3.15-slim WORKDIR /app diff --git a/hookjudge/requirements.lock b/hookjudge/requirements.lock index 0488a1f5..ed407287 100644 --- a/hookjudge/requirements.lock +++ b/hookjudge/requirements.lock @@ -16,7 +16,7 @@ fastapi==0.162.6 filelock==4.2.4 h11==0.16.0 httpcore==1.0.9 -httptools==0.8.0 +httptools==0.9.0 httpx==0.28.1 idna==3.19 iniconfig==2.3.0 @@ -31,14 +31,14 @@ opentelemetry-api==1.45.1 packageurl-python==0.17.6 packaging==26.3 pathspec==1.1.1 -pip-api==0.0.34 pip-requirements-parser==32.0.1 +pip_api==0.0.35 pip_audit==2.10.1 platformdirs==4.11.3 pluggy==1.6.0 py-serializable==2.1.0 -pydantic==2.13.4 -pydantic_core==2.46.4 +pydantic==2.14.3 +pydantic_core==2.50.4 Pygments==2.21.0 pyparsing==3.3.2 pytest==9.2.0 @@ -58,6 +58,6 @@ typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 uvicorn==0.54.0 -uvloop==0.22.1 +uvloop==0.23.0 watchfiles==1.2.0 websockets==17.0.1 diff --git a/hookprobe/Dockerfile b/hookprobe/Dockerfile index b7396619..9a4050d8 100644 --- a/hookprobe/Dockerfile +++ b/hookprobe/Dockerfile @@ -2,7 +2,7 @@ # deploy/mcp.example.json) + a lean read-only diagnostic core. Domain CLIs # (kubectl, db clients) stay opt-in below — extend with exactly what your # alerts need. -FROM python:3.14-slim +FROM python:3.15-slim ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ diff --git a/hookprobe/deploy/docker-compose.prod.yml b/hookprobe/deploy/docker-compose.prod.yml index 90828516..217108fe 100644 --- a/hookprobe/deploy/docker-compose.prod.yml +++ b/hookprobe/deploy/docker-compose.prod.yml @@ -54,7 +54,7 @@ services: # which needs the neighbouring deployment's compose — see # .agents/notes/proposed/2026-09-10-the-agent-can-read-the-provider-key-and-post-it-anywhere.md egress-proxy: - image: python:3.14-slim + image: python:3.15-slim container_name: hookprobe-egress restart: unless-stopped command: ["python3", "-u", "/app/proxy.py"] diff --git a/hookprobe/requirements.lock b/hookprobe/requirements.lock index dd56a86d..e3fc35ca 100644 --- a/hookprobe/requirements.lock +++ b/hookprobe/requirements.lock @@ -20,7 +20,7 @@ filelock==4.2.4 h11==0.16.0 httpcore==1.0.9 httpcore2==2.12.0 -httptools==0.8.0 +httptools==0.9.0 httpx==0.28.1 httpx2==2.12.0 idna==3.19 @@ -40,15 +40,15 @@ opentelemetry-api==1.45.1 packageurl-python==0.17.6 packaging==26.3 pathspec==1.1.1 -pip-api==0.0.34 pip-requirements-parser==32.0.1 +pip_api==0.0.35 pip_audit==2.10.1 platformdirs==4.11.3 pluggy==1.6.0 py-serializable==2.1.0 pycparser==3.0 -pydantic==2.13.4 -pydantic_core==2.46.4 +pydantic==2.14.3 +pydantic_core==2.50.4 Pygments==2.21.0 PyJWT==2.15.1 pyparsing==3.3.2 @@ -74,6 +74,6 @@ typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 uvicorn==0.54.0 -uvloop==0.22.1 +uvloop==0.23.0 watchfiles==1.2.0 websockets==17.0.1 diff --git a/hookrelay/Dockerfile b/hookrelay/Dockerfile index 5696cc15..1b4b2902 100644 --- a/hookrelay/Dockerfile +++ b/hookrelay/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.14-slim +FROM python:3.15-slim WORKDIR /app # Runtime dependencies only, pinned by the lock — pytest, ruff and mypy diff --git a/hookrelay/requirements.lock b/hookrelay/requirements.lock index d4d828db..e6cf0cf8 100644 --- a/hookrelay/requirements.lock +++ b/hookrelay/requirements.lock @@ -16,7 +16,7 @@ fastapi==0.162.6 filelock==4.2.4 h11==0.16.0 httpcore==1.0.9 -httptools==0.8.0 +httptools==0.9.0 httpx==0.28.1 idna==3.19 iniconfig==2.3.0 @@ -31,14 +31,14 @@ opentelemetry-api==1.45.1 packageurl-python==0.17.6 packaging==26.3 pathspec==1.1.1 -pip-api==0.0.34 pip-requirements-parser==32.0.1 +pip_api==0.0.35 pip_audit==2.10.1 platformdirs==4.11.3 pluggy==1.6.0 py-serializable==2.1.0 -pydantic==2.13.4 -pydantic_core==2.46.4 +pydantic==2.14.3 +pydantic_core==2.50.4 Pygments==2.21.0 pyparsing==3.3.2 pytest==9.2.0 @@ -59,6 +59,6 @@ typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 uvicorn==0.54.0 -uvloop==0.22.1 +uvloop==0.23.0 watchfiles==1.2.0 websockets==17.0.1