Skip to content

chore: move to Python 3.14, with the sandbox fixes it needs - #12247

Merged
MarkusNeusinger merged 13 commits into
mainfrom
chore/python-3.14
Oct 23, 2026
Merged

MarkusNeusinger merged 13 commits into
mainfrom
chore/python-3.14

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Moves the whole project from Python 3.13 to 3.14: requires-python, .python-version (3.14.9), mypy, all eight setup-python steps, the three service images (python:3.14-slim) and the languages table's runtime version. 3.13 receives security fixes only since October 2026; 3.14 gets bug fixes until October 2027; 3.15 waits for a lets-plot cp315 wheel.
  • This bump carries a renderer-sandbox change (section below): 3.14's new ctypes audit event broke every plot that imports scipy under the harness, and 3.14 added remote debugging and two evaluation routes the validator did not know.
  • One lock change beyond the floor: google-crc32c 1.7.1 → 1.9.0, because 1.7.1 publishes no cp314 wheel and the images carry no compiler.
  • ruff infers its target from requires-python, so its formatter now writes PEP 758 except A, B: (63 lines in 32 files, a commit of its own).

Sandbox change (renderer harness and validator)

What broke. CPython 3.14 raises the audit event ctypes.call_function for every call through a ctypes function pointer, ctypes' own cast, memmove and memset included; 3.13 raised none for those three. scipy's _lib._ccallback_c calls ctypes.cast while it is imported. PlotGuard refuses every ctypes.* event outside threadpoolctl, so on 3.14 every plot importing scipy, statsmodels, scikit-learn or seaborn failed: six harness tests and the renderer image's own seaborn render in ci-image.yml.

The fix, as shipped. PlotGuard lets ctypes.call_function through only for _ctypes._cast_addr (IMPORT_CTYPES_HELPERS) and only when the caller's nearest origin is an import (calls_import_ctypes_helper), the rule exec and frames already follow. DataGuard and ConvertGuard allow no foreign function at all.

Fable review: "sound with changes". The first draft let cast, memmove and memset through in all three guards, by address alone. Fable showed it handed plot code a raw in-process write primitive (ctypes.memmove/memset called directly) that 3.14's event had closed, and that the data and convert guards gained it for no legitimate use. All three narrowings were adopted:

  1. _cast_addr only: an audit-hook survey over the imports of numpy, pandas, matplotlib, scipy, scikit-learn and statsmodels found cast as the only helper any of them calls at import, from scipy/_lib/_ccallback.py.
  2. Import origin only: at scipy's import the first non-ctypes Python frame is <frozen importlib._bootstrap> (Cython module init has no Python frame), so nearest_origin is "import"; the same cast from plot code is "plot" and stays refused.
  3. PlotGuard only: data mode imports scipy before it arms, and neither data nor convert mode imports after.

Fable's open check, whether a library casts at render time from a plot-origin frame, is answered by a sweep: all 650 catalogue matplotlib and seaborn files, normalised as a session opening does, rendered under the narrowed guard with the sandbox flags. 603 render and none is refused for ctypes. The 47 failures (refused writes and os.mkdir, network fetches, missing qrcode and adjustText, plots that divide by an emptied title) fail identically on Python 3.13.12 with main's harness.

Two more 3.14-specific hardenings, in the same commits:

  • PEP 768 remote debugging: every sandbox argv carries -X disable-remote-debug (harness.INTERPRETER_FLAGS, duplicated in agents/renderer/executor.py and pinned by test_wire; -I ignores PYTHON_DISABLE_REMOTE_DEBUG), and both guards refuse sys.remote_exec and cpython.remote_debugger_script for a harness started without the flag. Verified: sys.remote_exec against the interpreter's own pid works on plain 3.14.9 and fails with "Remote debugging is not enabled" under the flag.
  • Validator: 3.14's dataclasses re-exports annotationlib, and from dataclasses import annotationlib plus get_annotations(f, eval_str=True) evaluated an arbitrary annotation string past the static validator (both runtime guards still refused it). annotationlib, get_annotations, call_evaluate_function, call_annotate_function and evaluate_forward_ref joined ESCAPE_ATTRIBUTES; the stdlib walk note records the 3.14.9 re-walk; refused fixtures pin both routes.

Fable's second review (of this PR): "sound with one change". PEP 734 subinterpreters escape the audit hook: a hook added with sys.addaudithook belongs to one interpreter, and creating, entering or running code in another raises no event the guard sees. With PlotGuard armed on 3.14, concurrent.interpreters.create().exec(...) wrote a file outside the outputs and the render exited 0; only the validator's import denylist stood in the way. Fixed in this PR: _interpreters.create, exec, run_string, run_func and call join UNAUDITED_CALLS and are replaced with refused_call before arming (concurrent.interpreters and InterpreterPoolExecutor reach them only through the module object), and concurrent, interpreters, InterpreterPoolExecutor, _interpreters, _interpqueues and _interpchannels join ESCAPE_ATTRIBUTES. Tests pin the three plot routes and a data run; the same plot writes its file with the previous harness and is refused with this one. Everything else Fable checked is sound: the import-origin _cast_addr allowance cannot be smuggled (frames cannot be forged, -I keeps the working directory off sys.path, writes are refused, ctypes attribute names are banned), only scipy raises ctypes.call_function at import, sys.remote_exec is covered three ways, and pathlib's new Path.copy/move decompose into audited calls.

Re-verified harness claims on 3.14.9 (side-by-side probe against 3.13.12, plus the 3.14 audit-event table): os.stat and its family, os.environ/os.getenv, function.__globals__, os.mkfifo, os.mknod, signal.signal and an import of a loaded module still raise no event. _posixsubprocess.fork_exec now raises one; it stays replaced. New 3.14 events: _posixsubprocess.fork_exec, cpython.PyConfig_Set, cpython.remote_debugger_script, ctypes.memoryview_at, sys.remote_exec.

Reviews

  • Fable (sandbox, twice): first draft "sound with changes", three narrowings adopted (section above); this PR "sound with one change", the subinterpreter fix applied.
  • Gemini 3.8 Flash (routine first review): "sound with changes", three low findings.
    • Applied: WorkflowState.load and from_stdin return Self | None instead of naming the class.
    • Skipped: the PEP 723 header requires-python = ">=3.13" of automation/scripts/canvas_gate_report.py. It and the one-off scripts/*.py import no project module (only _palette_common from scripts/, which the formatter does not touch) and all still parse on 3.13, so >=3.13 stays true.
    • Skipped: the "3.13" values in the dated JSON response samples of docs/reference/api.md, mcp.md and database.md (left alone on purpose, see below).
    • Applied (also raised by Fable): the project guide's anti-pattern line named the retired default.py layout and Python 3.10-3.13; it now states the one-file-per-library rule on 3.14.
  • Fable's two small follow-ups: executor.INTERPRETER_FLAGS duplicates harness.INTERPRETER_FLAGS because the renderer image cannot import the agents package; test_wire already pins them equal. The project-guide line is fixed (above).
  • Copilot: reviewed bffacb5, the head with every fix above: "No open issues", no inline comments. Since the round was clean and it already covered the subinterpreter fix, no further round was requested.
  • CI flake fixed on the way: Run Tests on bffacb5 failed test_local_backend_kills_the_container_on_cancellation once (only one of two containers had started 0.5 s in on the loaded runner). The test now cancels once both fake containers run instead of after a fixed sleep; 12 concurrent local runs passed before and after.

Files changed, by category

Category Files
Interpreter and lock .python-version, pyproject.toml (requires-python, mypy target, coverage comment), uv.lock
Images api/Dockerfile, agents/Dockerfile, agents/renderer/Dockerfile (base tag, UV_PYTHON, the pyarrow slimming path), tests/unit/agents/test_image.py (stage names)
Workflows python-version: '3.14' in ci-changelog, ci-lint, ci-migrations, daily-regen, impl-generate, impl-repair and sync-postgres; ci-image's pyarrow du path and harness commands (sandbox flags); the python_version comment in impl-generate. New setup-python (from .python-version) where repository Python ran on the runner's own python3: ci-tests (the existing step moves ahead of the spec check and runs on every event), impl-merge's write-back job, and review-retest's prep, review and aggregate jobs
Sandbox agents/anyplot/render/harness.py, agents/anyplot/render/runtimes/python.py, agents/renderer/executor.py, agents/evals/host_data.py, agents/anyplot/render/backends/local.py (docstring), agents/anyplot/code/validate.py, tests/image/convert_smoke.py, and their tests (test_harness_guard, test_validate, test_render, test_executor, test_wire, test_host_data)
Runtime data core/constants.py runtime_version 3.14 (sync-postgres seeds the languages table from it after merge)
Comments core/database/models.py, agentic/workflows/modules/regen/metadata.py, prompts/templates/metadata.yaml
Lint fallout agentic/workflows/modules/state.py: Optional["WorkflowState"] (UP045 under the py314 target) becomes Self | None
Formatter 32 files, except (A, B): → except A, B: only
Docs README.md, docs/development.md, docs/concepts/agent-network.md, docs/reference/repository.md, docs/reference/database.md, agentic/docs/project-guide.md + .github/copilot-instructions.md (kept in sync), prompts/plot-generator.md, agentic/commands/prime.md, agentic/commands/audit.md, agentic/commands/audit/backend-auditor.md, .claude/skills/verify-migrations/SKILL.md
Changelog changelog.d/python-3.14.md (Changed + Security)

Left alone on purpose: test fixtures that carry "3.13" as sample data, the dated JSON response samples in docs/reference/api.md, mcp.md and database.md, historical files (CHANGELOG.md, agentic/audits/, alembic revisions, the eval baseline's recorded python), the PEP 723 headers of the one-off scripts/*.py (>=3.13 still admits 3.14), and the history: example in prompts/templates/library-metadata.yaml.

Decisions

  • Patch 3.14.9. The newest 3.14 the current uv (0.13.5) offers, and the patch python:3.14-slim resolves to today. CI's uv downloads exactly the .python-version patch (the last ci-tests run had setup-python 3.13.16 and tested on the pinned 3.13.12), so the pin is what CI tests on. A local uv 0.10.2 only knows up to 3.14.3 but finds an installed 3.14.9 (uv self update is the clean fix).
  • Lock written with the latest uv, which keeps lock revision 5 (what Dependabot writes); uv 0.10.x rewrites it as revision 3.
  • Coverage. core = "sysmon" stays and is now only explicit (coverage's default on 3.14). sysmon measures branches on 3.14 (coverage 7.12, no "no-sysmon" fallback), but branch stays off because it changes the reported numbers.
  • PEP 758 formatter commit kept: CI's ruff format --check needs it, and requires-python >=3.14 must stay because of it.
  • No workflow step relies on the runner's own python3 any more. The local stack test found three paths that ran repository Python on it without setup-python: the ci-tests spec check (imports regen_gate), impl-merge's write-back (regen_writeback → regen_gate) and review-retest's three jobs (review_retest, review_provenance, regen_gate). They worked only because ubuntu-latest (26.04) ships 3.14.4; under 3.12 they raise SyntaxError. Each job now sets up Python from .python-version (3.14.9 is in setup-python's manifest for ubuntu-26.04). close_issue_if_complete.py and spec_pr_guard.py reach no 3.14-only module and stay on the runner's python3.
  • Local renderer image: the local backend mounts the checkout's harness into AGENT_RENDER_IMAGE, so an anyplot-renderer:dev built before this PR fails every render with SyntaxError: multiple exception types must be parenthesized. agents/README.md and the changelog fragment say to rebuild it.

Lock changes

Package Before After
google-crc32c 1.7.1 1.9.0

Everything else keeps its version, plotting libraries included (the version-sources check of #12240 is unaffected). A scan of the lock for packages without a Linux x86_64 wheel usable on 3.14 finds only esprima and matplotlib-venn (pure-Python sdists, plotting extra only, as before) and pywin32 (Windows only).

Smoke renders on 3.14.9

The first five specs alphabetically for every Python library, both themes, MPLBACKEND=Agg ANYPLOT_THEME=<theme> python -P <lib>.py from a scratch directory with the plotting extra (impl-generate's command). Each cell is light / dark.

Spec matplotlib seaborn plotly altair plotnine bokeh pygal letsplot
acf-pacf pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass
alluvial-basic pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass
alluvial-opinion-flow pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass
andrews-curves pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass
arc-basic pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass pass / pass

80 of 80 PNGs written. Bokeh needs a working chromedriver on PATH (the local WSL one is a snap stub; CI runners ship one).

Images (local docker build)

  • agents/renderer/Dockerfile: builds on Debian 13 with CPython 3.14.9; pyarrow slimmed 167 MB → 99 MB; font-cache bake and check_fonts pass. ci-image's steps: the seaborn render (3200×1800, probe written) and the multi-script render (no missing glyph) pass with the final harness; the convert smoke passes every format; no ADK; /status reports 3.5.0; a render without a launcher is refused with 503. Before the harness fix the seaborn render failed in the image with ctypes.call_function.
  • api/Dockerfile: builds on 3.14.9; google-crc32c 1.9.0 installs from its wheel (C implementation); api.main imports.
  • agents/Dockerfile: not built locally; CI's image job builds it.

Plan

N/A

Test plan

  • uv run ruff check . and uv run ruff format --check .
  • uv run --extra typecheck mypy api core --pretty
  • uv run python -m tools.changelog check --base origin/main
  • Full suite as CI runs it (unit, integration, e2e skipped without DATABASE_URL) on the final code: 12,302 passed, 0 failed
  • Smoke renders: 80 of 80 (table above)
  • Guard sweep: 650 catalogue files, 603 render, no ctypes refusal, the 47 failures identical on 3.13.12
  • Renderer and API images built locally and smoked (above)
  • CI green on b815e2d (13 of 13 checks)
  • CI green on the final head
  • codecov/patch/backend stays red on purpose (accepted by the lead): the three new harness lines it flagged are now covered by in-process tests, and the 29 lines still missing are pre-existing error handlers that the PEP 758 formatter only rewrote (except (A, B): → except A, B:), which no test reached before this PR either.

Unverified: workflows have no local verification loop. The python-version changes and the new setup-python steps in impl-merge's write-back and review-retest are only observable on real pipeline runs; ci-tests exercises its own change on this PR. /languages keeps reporting Python 3.13 until sync-postgres runs on the next plots/ or alembic/ push and seeds runtime_version 3.14 from core/constants.py. The agents image was not built locally; CI's image job builds it.

Noticed, not acted on: two deprecation warnings for later Python releases, google-genai's typing._UnionGenericAlias (removed in 3.17) and uvloop 0.22.1's asyncio.iscoroutinefunction (removed in 3.16). The local stack test (core, API parity with main on 61 routes, the agents service with four real runs, the sandbox guards, 24 catalogue renders, the frontend and all three images) passed.

@MarkusNeusinger MarkusNeusinger changed the title chore: move the project to Python 3.14, with the sandbox fixes it needs chore: move to Python 3.14, with the sandbox fixes it needs Oct 23, 2026
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
Fable's review of #12247: PEP 734 subinterpreters escape the audit hook.
A hook added with sys.addaudithook belongs to one interpreter, and
creating, entering or running code in another raises no event the guard
sees; armed PlotGuard on 3.14, `concurrent.interpreters.create().exec(...)`
wrote a file outside the outputs with exit 0. The validator's import
denylist was the only barrier.

- harness: `_interpreters.create`, `exec`, `run_string`, `run_func` and
  `call` join UNAUDITED_CALLS and are replaced with `refused_call` before
  arming (`_interpreters` is imported then if it is not loaded).
  `concurrent.interpreters` and `InterpreterPoolExecutor` reach them only
  through the module object, so both are covered.
- validator: `concurrent`, `interpreters`, `InterpreterPoolExecutor`,
  `_interpreters`, `_interpqueues` and `_interpchannels` join
  ESCAPE_ATTRIBUTES.
- tests: `_interpreters.exec`, `concurrent.interpreters` exec and
  `InterpreterPoolExecutor` routes are refused in plot mode (the last two
  skip below 3.14), and creating a subinterpreter fails a data run.
  Verified the route writes its file with the previous harness and is
  refused with this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
Gemini Flash's first review of #12247: `load` and `from_stdin` return
`cls(...)`, so `Self | None` (PEP 673) is the subclass-safe annotation
instead of naming the class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
The anti-pattern line still named the retired default.py layout and
Python 3.10-3.13 (Fable's review of #12247).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
@MarkusNeusinger
MarkusNeusinger requested a balanced review from Copilot October 23, 2026 06:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Sandbox security boundaries and production runtime changes require final human review and confirmation that CI passes on the final head.

No open issues 🎉

What changed in this PR

Moves anyplot’s services, development environment, and automation to Python 3.14, with sandbox changes needed to preserve rendering compatibility and isolation.

Changes:

  • Updates the Python floor, interpreter pin, service images, workflow runtimes, and documentation.
  • Narrows ctypes allowances and blocks remote debugging, subinterpreter execution, and additional annotation-evaluation routes.
  • Adds sandbox regression coverage and adopts Python 3.14 exception syntax.
File Description
tests/​unit/​agents/​test_image.py Updates expected Docker stages to Python 3.14.
tests/​unit/​agents/​runtime/​test_render.py Checks sandbox flags across local execution modes.
tests/​unit/​agents/​runtime/​test_harness_guard.py Adds ctypes, remote-debugging, and subinterpreter regression tests.
tests/​unit/​agents/​renderer/​test_wire.py Pins interpreter-flag parity between components.
tests/​unit/​agents/​renderer/​test_executor.py Checks remote-debugging flags in renderer commands.
tests/​unit/​agents/​evals/​test_host_data.py Checks host-data interpreter flags.
tests/​unit/​agents/​data/​test_warning_codes.py Adopts Python 3.14 exception syntax.
tests/​unit/​agents/​code/​test_validate.py Tests rejection of annotation-evaluation routes.
tests/​image/​convert_smoke.py Disables remote debugging during conversion smoke tests.
README.md Updates the documented Python requirement.
pyproject.toml Raises Python and mypy targets; clarifies coverage behavior.
prompts/​templates/​metadata.yaml Updates the Python-version example comment.
prompts/​plot-generator.md Updates Python versions in generated-header examples.
docs/​reference/​repository.md Updates Python-version examples.
docs/​reference/​database.md Updates a schema example comment.
docs/​development.md Updates prerequisites and coverage guidance.
docs/​concepts/​agent-network.md Documents Python 3.14 and sandbox launch flags.
core/​database/​models.py Updates runtime-version comments only.
core/​constants.py Updates the registered Python runtime version.
changelog.d/​python-3.14.md Records the runtime migration and sandbox hardening.
automation/​scripts/​sync_to_postgres.py Adopts Python 3.14 exception syntax.
automation/​scripts/​review_retest.py Adopts Python 3.14 exception syntax.
automation/​scripts/​review_provenance.py Adopts Python 3.14 exception syntax.
automation/​scripts/​regen_gate.py Adopts Python 3.14 exception syntax.
api/​routers/​insights.py Adopts Python 3.14 exception syntax.
api/​routers/​agent.py Adopts Python 3.14 exception syntax.
api/​Dockerfile Migrates build and runtime stages to Python 3.14.
agents/​renderer/​wire.py Adopts Python 3.14 exception syntax.
agents/​renderer/​executor.py Adds sandbox interpreter flags and updates exception syntax.
agents/​renderer/​Dockerfile Updates Python stages, interpreter path, and pyarrow path.
agents/​renderer/​auth.py Adopts Python 3.14 exception syntax.
agents/​main.py Adopts Python 3.14 exception syntax.
agents/​evals/​report.py Adopts Python 3.14 exception syntax.
agents/​evals/​prep.py Adopts Python 3.14 exception syntax.
agents/​evals/​matrix.py Adopts Python 3.14 exception syntax.
agents/​evals/​host_data.py Uses shared sandbox interpreter flags.
agents/​Dockerfile Migrates agents-service stages to Python 3.14.
agents/​anyplot/​tools/​session.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​sub_agents/​adapter.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​render/​runtimes/​python.py Applies sandbox flags to plot, data, and conversion commands.
agents/​anyplot/​render/​harness.py Restricts ctypes allowances and blocks new execution routes.
agents/​anyplot/​render/​gates.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​render/​convert.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​render/​backends/​remote.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​render/​backends/​local.py Updates command documentation and exception syntax.
agents/​anyplot/​plugins/​ledger.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​dataset_context.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​data/​prepare.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​data/​parse.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​data/​generate.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​code/​validate.py Bans annotation-evaluation and subinterpreter access names.
agents/​anyplot/​code/​regions.py Adopts Python 3.14 exception syntax.
agents/​anyplot/​code/​data_code.py Adopts Python 3.14 exception syntax.
agentic/​workflows/​modules/​state.py Uses Self | None for classmethod return types.
agentic/​workflows/​modules/​regen/​picker.py Adopts Python 3.14 exception syntax.
agentic/​workflows/​modules/​regen/​metadata.py Updates the interpreter-output example comment.
agentic/​docs/​project-guide.md Updates runtime guidance and implementation-layout documentation.
agentic/​commands/​prime.md Updates the documented Python baseline.
agentic/​commands/​audit/​backend-auditor.md Updates Python modernization guidance.
agentic/​commands/​audit.md Updates Python examples in audit criteria.
.python-version Pins Python 3.14.9.
.github/​workflows/​sync-postgres.yml Selects Python 3.14 for database synchronization.
.github/​workflows/​impl-repair.yml Selects Python 3.14 for implementation repair.
.github/​workflows/​impl-generate.yml Updates Python selection and its metadata comment.
.github/​workflows/​daily-regen.yml Selects Python 3.14 for daily regeneration.
.github/​workflows/​ci-tests.yml Selects Python 3.14 for tests.
.github/​workflows/​ci-migrations.yml Selects Python 3.14 for migration checks.
.github/​workflows/​ci-lint.yml Selects Python 3.14 for lint checks.
.github/​workflows/​ci-image.yml Updates sandbox smoke commands and the pyarrow inspection path.
.github/​workflows/​ci-changelog.yml Selects Python 3.14 for changelog checks.
.github/​copilot-instructions.md Updates documented runtime and header examples.
.claude/​skills/​zweitmeinung/​gemini_review.py Adopts Python 3.14 exception syntax.
.claude/​skills/​verify-migrations/​SKILL.md Updates the separate-interpreter explanation.
.claude/​skills/​review-regen-week/​collect_week.py Adopts Python 3.14 exception syntax.

Review effort: Balanced

MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
CI's Run Tests on #12247 failed test_local_backend_kills_the_container_on_
cancellation once: after a fixed 0.5 s sleep only the light theme's
container had started on the loaded runner (coverage, four workers), so only
one kill was recorded. The fake docker now notes each run and the test
cancels once both themes run (up to 10 s), which is what it asserts on.
12 concurrent local runs pass before and after; the race is the runner's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
…llback

codecov/patch/backend on #12247 listed three new harness lines that only a
subprocess harness run reached (coverage measures the test process): the
`_interpreters` branch of `disable_unaudited_calls` and the ImportError
fallback of `ctypes_helper_addresses`. Both now run in-process without
arming a hook: one test makes `_ctypes` unimportable, the other imports a
fresh `_interpreters` with the original's attributes registered for
restore and checks all five calls are replaced and refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
The local stack test of #12247: three workflow paths ran repository Python
on the runner image's own python3 without setup-python, and the code they
import now uses PEP 758 `except A, B:`, which does not parse before 3.14
(reproduced as a SyntaxError under 3.12). They worked only because
ubuntu-latest ships 3.14.4 today.

- ci-tests: the existing setup-python step moves ahead of the spec
  characteristic check and runs on every event (spec_characteristics_lint
  imports regen_gate).
- impl-merge writeback: setup-python after checkout (regen_writeback imports
  regen_gate).
- review-retest prep, review and aggregate: setup-python after checkout
  (review_retest, review_provenance, regen_gate).
All read `.python-version` (3.14.9, in setup-python's manifest for
ubuntu-26.04). close_issue_if_complete and spec_pr_guard reach no 3.14-only
module and stay as they are.

Docs: agents/README.md and the changelog fragment tell developers to
rebuild a local anyplot-renderer:dev image, because the local renderer
mounts the checkout's harness, which no longer parses on a 3.13 image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
MarkusNeusinger and others added 13 commits October 23, 2026 12:05
3.13 receives security fixes only since October 2026; 3.14 gets bug fixes
until October 2027. 3.15 waits for a lets-plot cp315 wheel.

- requires-python >=3.14, mypy python_version 3.14, .python-version 3.14.9
  (the newest patch current uv offers; CI's uv downloads exactly this patch)
- uv.lock regenerated with the latest uv (keeps lock revision 5); the only
  version change is google-crc32c 1.7.1 -> 1.9.0, because 1.7.1 publishes no
  cp314 wheel and the images carry no compiler
- python:3.14-slim and UV_PYTHON=/usr/local/bin/python3.14 in the api,
  agents and renderer images; the renderer's pyarrow slimming path and the
  CI image job's du path follow; test_image.py asserts the new stage names
- python-version '3.14' in all eight setup-python steps
- languages table runtime_version 3.14 (core/constants.py)
- coverage comment: sysmon is coverage's default on 3.14 and measures
  branches there; branch stays off because it changes the reported numbers
- harness docstrings re-verified on 3.14.9 (os.stat family, os.environ,
  __globals__, mkfifo/mknod, signal.signal still raise no event;
  _posixsubprocess.fork_exec now raises one and stays replaced)
- agentic/workflows/modules/state.py: Optional["WorkflowState"] becomes
  WorkflowState | None (ruff's py314 target flags UP045; PEP 649 defers the
  annotation, so no quotes are needed)
- docs, prompts and skill text that state the current version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…P 758)

ruff infers its target from requires-python, so with >=3.14 its formatter
writes `except A, B:` instead of `except (A, B):` (PEP 758, new in 3.14).
63 lines in 32 files, nothing else. Every place this code runs is 3.14:
the project venv, the three images, and the runner's system python3
(ubuntu-latest = 26.04 ships 3.14.4) that review_retest and the spec lint
use before setup-python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python 3.14 raises the audit event `ctypes.call_function` for every call
through a ctypes function pointer, ctypes' own `cast`, `memmove` and
`memset` included; 3.13 raised none for them. scipy's `_lib._ccallback_c`
calls `ctypes.cast` while it is imported, so PlotGuard refused every plot
that imports scipy, statsmodels or scikit-learn (six harness tests and the
renderer image's seaborn render failed). Every guard now lets
`ctypes.call_function` through only for the three helper addresses
(`CTYPES_HELPERS`, read from `_ctypes` before arming), which is 3.13's
behaviour; `string_at`, `wstring_at`, `memoryview_at` and every other
foreign function stay refused. A unit test per guard pins it.

Two more 3.14-specific gaps closed:
- PEP 768 remote debugging: the sandbox interpreter runs with
  `-X disable-remote-debug` (`harness.INTERPRETER_FLAGS`, mirrored in the
  renderer executor and pinned by test_wire), and both guards refuse
  `sys.remote_exec` and `cpython.remote_debugger_script` for a harness
  started without it.
- validator: 3.14's `dataclasses` re-exports `annotationlib`, whose
  `get_annotations(eval_str=True)` evaluates annotation strings, and
  `typing.evaluate_forward_ref` is new; both passed the static validator.
  The five names join ESCAPE_ATTRIBUTES, the stdlib walk note records the
  3.14.9 re-walk, and refused fixtures pin both routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Fable review of the first draft found the three-address allowlist
broader than needed: it let plot code call ctypes.memmove, memset and cast
directly, a write primitive that 3.14's new `ctypes.call_function` event
had closed, and the data and convert guards gained it for no legitimate
use (data mode imports scipy before arming; neither mode imports after).

- PlotGuard only: `ctypes.call_function` passes only for `_ctypes._cast_addr`
  (`IMPORT_CTYPES_HELPERS`) and only when the caller's nearest origin is an
  import (`calls_import_ctypes_helper`), the rule exec and frames follow.
  scipy's import-time cast is the only helper call any allowed library makes
  at import (Fable's audit-hook survey).
- DataGuard and ConvertGuard refuse every foreign function again.
- Tests: the data and convert guards refuse all helpers; the plot guard lets
  only an import's cast through; cast, memmove and memset from plot code and
  cast and memmove from a data snippet are refused end to end.
- A sweep of all 650 catalogue matplotlib and seaborn files under the
  narrowed guard: 603 render, no ctypes refusal; the 47 failures fail
  identically on 3.13.12 with main's harness.
- backends/local.py docstring shows the sandbox flags.
- changelog fragment: a Security entry for the sandbox change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
Fable's review of #12247: PEP 734 subinterpreters escape the audit hook.
A hook added with sys.addaudithook belongs to one interpreter, and
creating, entering or running code in another raises no event the guard
sees; armed PlotGuard on 3.14, `concurrent.interpreters.create().exec(...)`
wrote a file outside the outputs with exit 0. The validator's import
denylist was the only barrier.

- harness: `_interpreters.create`, `exec`, `run_string`, `run_func` and
  `call` join UNAUDITED_CALLS and are replaced with `refused_call` before
  arming (`_interpreters` is imported then if it is not loaded).
  `concurrent.interpreters` and `InterpreterPoolExecutor` reach them only
  through the module object, so both are covered.
- validator: `concurrent`, `interpreters`, `InterpreterPoolExecutor`,
  `_interpreters`, `_interpqueues` and `_interpchannels` join
  ESCAPE_ATTRIBUTES.
- tests: `_interpreters.exec`, `concurrent.interpreters` exec and
  `InterpreterPoolExecutor` routes are refused in plot mode (the last two
  skip below 3.14), and creating a subinterpreter fails a data run.
  Verified the route writes its file with the previous harness and is
  refused with this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
Gemini Flash's first review of #12247: `load` and `from_stdin` return
`cls(...)`, so `Self | None` (PEP 673) is the subclass-safe annotation
instead of naming the class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
The anti-pattern line still named the retired default.py layout and
Python 3.10-3.13 (Fable's review of #12247).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
CI's Run Tests on #12247 failed test_local_backend_kills_the_container_on_
cancellation once: after a fixed 0.5 s sleep only the light theme's
container had started on the loaded runner (coverage, four workers), so only
one kill was recorded. The fake docker now notes each run and the test
cancels once both themes run (up to 10 s), which is what it asserts on.
12 concurrent local runs pass before and after; the race is the runner's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
…llback

codecov/patch/backend on #12247 listed three new harness lines that only a
subprocess harness run reached (coverage measures the test process): the
`_interpreters` branch of `disable_unaudited_calls` and the ImportError
fallback of `ctypes_helper_addresses`. Both now run in-process without
arming a hook: one test makes `_ctypes` unimportable, the other imports a
fresh `_interpreters` with the original's attributes registered for
restore and checks all five calls are replaced and refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
The local stack test of #12247: three workflow paths ran repository Python
on the runner image's own python3 without setup-python, and the code they
import now uses PEP 758 `except A, B:`, which does not parse before 3.14
(reproduced as a SyntaxError under 3.12). They worked only because
ubuntu-latest ships 3.14.4 today.

- ci-tests: the existing setup-python step moves ahead of the spec
  characteristic check and runs on every event (spec_characteristics_lint
  imports regen_gate).
- impl-merge writeback: setup-python after checkout (regen_writeback imports
  regen_gate).
- review-retest prep, review and aggregate: setup-python after checkout
  (review_retest, review_provenance, regen_gate).
All read `.python-version` (3.14.9, in setup-python's manifest for
ubuntu-26.04). close_issue_if_complete and spec_pr_guard reach no 3.14-only
module and stay as they are.

Docs: agents/README.md and the changelog fragment tell developers to
rebuild a local anyplot-renderer:dev image, because the local renderer
mounts the checkout's harness, which no longer parses on a 3.13 image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
…d of uv

The step now runs on setup-python's 3.14 (regen_gate's PEP 758 syntax);
the test asserted the old order and failed on 658fd77. It now pins the
new order and keeps the real invariant: no uv, no project install first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza
@MarkusNeusinger
MarkusNeusinger merged commit 69b084a into main Oct 23, 2026
15 of 16 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the chore/python-3.14 branch October 23, 2026 10:16
MarkusNeusinger added a commit that referenced this pull request Oct 23, 2026
## Summary
- PR 2 of the adaptable-code rollout
([design](https://github.com/MarkusNeusinger/anyplot/blob/main/docs/concepts/adaptable-code.md),
"Rollout" item 2; PR 1 was #12244): the Data block convention in the
generator and in all 15 library prompts, a Python self-check (Step 3c)
in generation and repair, a record-only smoke run and predecessor render
in `impl-generate.yml` and `impl-repair.yml`, a record-only adaptability
check in `impl-review.yml`, and the identity-based adapt route plus the
adapt regression in the regen gate.
- Score-neutral: the review prompt and the rubric are unchanged, every
new step is record-only (never a raw `exit 1`), and the review-free path
stays off behind the repository variable `ADAPT_REVIEW_FREE`.
- Checks are Python only (the 8 Python libraries). R, Julia and
JavaScript get the convention in their first-generation prompts and keep
today's regeneration mindset; every record carries `language`, so their
checkers plug in later without format changes (the "all 15 libraries"
goal).

## Plan
Design: `docs/concepts/adaptable-code.md` ("The convention", "Other
libraries", "The checks", "Pipeline changes", "The adapt route",
"Rollout" item 2). Four parts were built against one interface contract
and merged here with `--no-ff`; the merge commits keep each part's
history.

### A, prompts (`eb3d074fa`)
- New `prompts/adaptable-code.md` holds the convention once: the six
zones, the `# Data`/`# Plot` markers and `# role:` grammar, the
plot-code rules, the data shapes, the bar-grouped example, and the R,
Julia and JavaScript forms. `plot-generator.md` and Step 1 of
`impl-generate-claude.md` reference it.
- `plot-generator.md`: the template, Structure and Data Generation
Strategy follow the six zones; Data storytelling gets rule-chosen
callout targets; `bbox_inches='tight'` is gone.
`default-style-guide.md`: "list all eight hues and use the first n", no
`bbox_inches='tight'`.
- All 15 library prompts get a "Data block" section; the `[:N]` palette
slices are gone from matplotlib and seaborn; Chart.js no longer recycles
colours with `i % length`.
- `impl-generate-claude.md`: Step 3c (`python -m core.smoke_run
selfcheck`, at most three attempts, never blocking), the commit line
`Adaptability self-check: pass|fail (...), attempt N`, the five
regen-mindset amendments phrased "For Python (`LANGUAGE=python`)", a
"Converting a Python file" bullet that aims at an identical render.
`impl-repair-claude.md`: self-check lines are real defects; Step 5b
reruns the self-check.

### B, generation side (`416065feb`, `3425ac00b`)
- `core/smoke_run.py` (`run`, `pair`, `selfcheck`): one process per
perturbed table and theme, process groups killed on timeout, errors
mapped back to the original line, a stale-label hook for matplotlib and
seaborn, and `pair`, which renders the new source and then the
predecessor in one environment and compares the raw PNGs.
- `core/perturbation.py`: `missing_number` (NaN in every `float64`
column, binding) and `missing_nullable` (`<NA>` in `Int64`, `boolean`
and `string` columns, advisory).
- `impl-generate.yml` "Adaptability smoke run (record-only)" and the
private `adapt/` staging in "Upload to GCS Staging"; `impl-repair.yml`
reruns the smoke run and replaces or removes `adapt/new-smoke.json`.

### C, review side (`4b710e2c0`)
- `impl-review.yml` "Adaptability check (record-only)": the workflow-ref
checker mirror on the PR file and, on a regeneration, its predecessor;
`automation/scripts/adapt_record.py` builds
`/tmp/anyplot-adapt-{new,prev}.json`; `regen_gate.py adapt-route` before
the review; the review is skipped only when `ADAPT_REVIEW_FREE` is
`true` and every condition holds.
- Review-free wiring, the `regen:adapt` count label, carried metadata
(`review.carried`), and the staging download narrowed to `plot-*` so
`adapt/` never reaches the blind reviewer. `impl-merge.yml` promotes
only `plot-*`. `regen_writeback.py` removes `review.carried` on a
write-back.

### D, gate (`0438a60a4`)
- `adapt_assess` with the contract's 20 ordered conditions;
`adapt_identity` replaces the keeps `no_visible_improvement`,
`no_defect_improvement` and `below_tolerance`; `--review-skipped` merges
a counted route before the score checks; `adapt_regression` turns any
merge into keep. The record gains an `adapt` block (`RECORD_VERSION`
stays 1), the outputs and notice gain `adapt_counted` and `adapt_why`.
The gate stays stdlib-only and reads identity from impl-generate's
`pair.json`.

### Integration (`db865896b`)
- `impl-review.yml` gets one unconditional "Set up Python" step
(`python-version-file: .python-version`) before the first regen-tools
call; part C's mirror-only step is gone and every tool runs on plain
`python3`. Since #12247 the gate, `review_provenance.py` and the mirror
use PEP 758 `except A, B:`, and the runner image's Python is not pinned.
- The Step 3c log line gains `smoke_why`, so the adapt record counts a
skipped first attempt as a pass only for a shape skip, as `selfcheck`'s
own exit 0 does.
- Docs: the pipeline diagram and a table of the adaptability steps
(`docs/workflows/overview.md`), `adapt_record.py`
(`automation/scripts/README.md`), `core/smoke_run.py`
(`docs/reference/repository.md`).

### Review fixes (`05ec4a82b`, `602dd40c5`, `4fd7f7c57`)
- Copilot: an adapt record without valid `static` and `smoke` blocks is
`record_invalid` (fail closed); the Chart.js and ECharts Data block
examples run as written; the "ninth group fails loudly" rule names the
idioms it holds for (lookup by position or group) in
`prompts/adaptable-code.md` and `prompts/default-style-guide.md`.
- Gemini and part D's own note: the route needs a `pair.json` rendered
from the very sources the records checked (`code_sha256` on both sides),
else the new reason token `pair_stale`.

### Second opinion (Gemini 4 Argon + Fable, `1e75be40a`..`a423e7b69`)
- The gate rejects a record whose `status` does not follow from its
`static` and `smoke` blocks (`record_invalid`), and `pair.json` must
name its `library`.
- A predecessor whose static check passed against a new source whose
static check fails is an adapt regression even without a fresh
predecessor smoke run.
- `decide --adapt-route-held`, the record field
`adapt.route_pre_review`, the output `adapt_overruled` and the label
`regen:adapt-overruled` make the review's overrules of the route
countable (O1).
- The post-score re-dispatch also covers the review-free path, with the
stored score.
- `adapt/` is staged with `gsutil rsync -r` so `new/` and `prev/` keep
their names.
- `execution_env` drops `GITHUB_*`, `ACTIONS_*`, `RUNNER_*` and `GH_*`
before running an implementation.
- An empty perturbation set has its own token
`no_perturbations:<shape>`, read the same way by `selfcheck` and the
record.
- One palette name, `IMPRINT_PALETTE`, in the convention and its
example; the `IMPRINT` alias is stated once.
- Known gap, unchanged: the early verdict label lands before "Update
metadata and implementation header"; a metadata push failure after it is
left to the watchdog (pre-existing on the normal path).

## Test plan
- [x] `uv run ruff check .` and `uv run ruff format --check .`: clean.
- [x] `uv run --extra typecheck --extra agents mypy api core agents`: no
issues in 151 source files.
- [x] `uv run pytest tests/unit tests/integration -n 4`: 13158 passed, 3
skipped (seaborn is not installed locally). That is 443 more tests than
`main`: `test_smoke_run.py` 34, `test_adapt_generate_steps.py` 36,
`test_adapt_review_steps.py` 85, `test_adapt_record.py` 66, plus
additions to `test_regen_gate.py`, `test_perturbation.py`,
`test_regen_writeback.py` and `test_prompts.py`.
- [x] `uv run python -m tools.changelog check --base origin/main`:
passes.
- [x] actionlint 1.7.12 with shellcheck 0.11.0 on the four changed
workflows: 87 findings on `main` and on this branch, the same set (only
line offsets in impl-generate's upload step moved).
- [x] End to end on one real pair outside the repository: bar-grouped
matplotlib, a predecessor with a three-hue palette (static rule 5 fails)
against the converted fixture. Part B's `run`, `pair` and `selfcheck`
fed part C's mirror and `adapt_record.py`, which fed part D's
`adapt-route` and `decide --review-skipped`: `route=true`, merge
`adapt_identity`, identity 0.0000 in both themes, a valid gate record.
The new side's smoke run passed all 7 binding tables; `missing_nullable`
failed as advisory (`float() argument ... 'NAType'` in `ax.bar`).
- [ ] After the merge: the first Python regeneration's `adapt_smoke`,
`adapt_check`, `adapt_route`, `adapt_review` and `regen_gate ...
adapt_*` notices, and the `adapt/` folder of its `regen-pair` artifact.

## Known gaps
Workflow changes have no branch run: claude-code-action refuses to run
on a branch whose PR changes its own workflows, so the new steps are
first exercised on `main`. Locally they are covered by unit tests that
run the exact step scripts against throwaway repositories with fake
`gsutil` and `gh`, under `bash -eo pipefail`.

- **First real runs.** The MUI X pilot of 8 forced regenerations is the
first real run of the new prompts, and the lead watches it. MUI X is
JavaScript and `regen:forced`, so it exercises the convention text, not
the Python steps; the first Python regeneration after the merge is the
first run of the smoke run, the record-only check and the adapt route.
The daily regeneration stays paused until this merges ("Rollout" step
3).
- **Not verified until a real run:** `gsutil rm -r` on a missing prefix,
`rsync -r` of `adapt/` to staging, `cp -r` back in impl-review, and
whether bucket-level IAM keeps `adapt/` private; impl-merge's
non-recursive `cp "${STAGING}/plot-*"`; the runtime of the Chrome-driven
libraries (bokeh, plotly) inside the 20-minute step timeout (estimate 1
to 3 minutes); the seaborn, plotly, altair, plotnine, bokeh, pygal and
letsplot paths against the runner (matplotlib ran locally);
`setup-python` with `.python-version` in impl-review;
`vars.ADAPT_REVIEW_FREE` resolution; the review-free path end to end.
- **Runner exposure.** The smoke executions run model-written code again
after "Create library metadata file" has the workflow token in
`.git/config`, the same exposure as the generator's own Step 3 renders.
The runner is not an isolation boundary; the protections are the
workflow-ref copy of `core/`, the new-then-predecessor render order, and
the owner-controlled switch.
- **Missing-value coverage.** The loader cast turns integer columns into
`Int64`, so an integer-valued sample (the design's own example) gets no
binding NaN table, only the advisory `<NA>` one.

## Open decisions for the owner
- **O1, review-free switch.** It stays off. The evidence is the
route-held PRs. `regen:adapt` marks a merge on which the adapt route
held before and through the review. `regen:adapt-overruled` marks a PR
whose route held before the review and whose review floor then turned it
off, on a merge or a keep; its gate record has `adapt.route_pre_review:
true`. Turn the switch on (`gh variable set ADAPT_REVIEW_FREE --body
true`) only when the overruled share stays near zero over at least 50
route-held PRs:
  ```
gh pr list --state merged --label regen:adapt --limit 1000 --json number
--jq length
gh pr list --state all --label regen:adapt-overruled --limit 1000 --json
number --jq length
  ```
The alternative is an automatic flip once 50 route-held PRs show no
overruled one.
- **O2, nullable integers.** `missing_nullable` stays advisory: in part
A's idiom run it failed 5 of the 8 Python libraries, and it fails the
design's reference example. Either the convention allows
`.astype(float)` on drawn nullable-integer columns, or the loader
changes.
- **Review-retest arm.** This PR edits `prompts/default-style-guide.md`
(two code-only sentences) and adds a "Data block" section to every
library prompt, and the reviewer reads both. `CLAUDE.md` asks for a
candidate `review-retest.yml` arm on such edits; the design says none is
needed, because no criterion, rubric line or review-prompt line changes.
No arm was dispatched; dispatch one if you want the measurement before
merging.
- **Local helpers.** `/regen` and `/update` list
`prompts/adaptable-code.md` but have no Step 3c, so a local regeneration
is not self-checked.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants