Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions .github/workflows/ci-hookjudge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci-hookprobe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
6 changes: 3 additions & 3 deletions deploy/docker-compose.work.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion deploy/sidecars/common.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion hookjudge/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.14-slim
FROM python:3.15-slim

WORKDIR /app

Expand Down
10 changes: 5 additions & 5 deletions hookjudge/requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion hookprobe/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion hookprobe/deploy/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
10 changes: 5 additions & 5 deletions hookprobe/requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion hookrelay/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 5 additions & 5 deletions hookrelay/requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Loading