Repository navigation
feat(docker): bake the Pi CLI into the runtime kit - #236
migheorghe wants to merge 1 commit into
Conversation
docker/Dockerfile installs Pi at PI_VERSION, but docker/Dockerfile.runtime did not, so `--driver docker --type pi` failed in every injected task image with "The 'pi' CLI was not found on PATH". The kit now installs the same package at the same pin under /opt/coder-eval/node, and a parity test keeps the two PI_VERSION pins equal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RarvxSsr4K2KkBisTbXxQY
|
Claude finished @migheorghe's task in 1m 27s —— View job Code Review in Progress
|
uipreliga
left a comment
There was a problem hiding this comment.
Review: feat(docker): bake the Pi CLI into the runtime kit
PR #236 by @migheorghe · feat/pi-in-runtime-kit → main · OPEN · reviewed against b1fc1bd
Change class: simple — adds one pinned npm install step to the runtime kit, mirrored from docker/Dockerfile, plus a pin-parity test and doc lines; no Python runtime code path changes
coder_eval is in very good condition (9.9/10): no finding can change a task's score or final_status for the same agent output, and the type safety, security, architecture and evaluation-harness axes are clean; the only runtime risk is a low-severity Node-engine mismatch in the Pi runtime kit, and the other findings are test duplication and doc cleanup in the Pi changes, so the codebase is ready to ship after the small fixes below.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.3 / 10 | 0 | 0 | 1 | 2 | Pi pin-parity test copies the Claude pin test and its Dockerfile ARG-pin parser again |
| 2. Type Safety | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 3. Test Health | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 4. Security | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Architecture & Design | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 6. Error Handling & Resilience | 9.9 / 10 | 0 | 0 | 0 | 1 | The Pi install accepts an unsupported Node engine with only a warning, and the pi --version gate cannot catch it |
| 7. API Surface & Maintainability | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 8. Evaluation Harness Quality | 10 / 10 | 0 | 0 | 0 | 0 | — |
Overall Score: 9.9 / 10 · Weakest Axis: Code Quality & Style at 9.3 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 1 · 🔵 3 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
- [Axis 1] Pi pin-parity test copies the Claude pin test and its Dockerfile ARG-pin parser again (
tests/test_image_from_dockerfiles.py:497) — The new test adds a nesteddef _pin(df: Path) -> str | None:(line 497) that differs from the one in test_claude_code_version_pin_matches_framework (line 473) only in its regex:re.match(r"\s*ARG PI_VERSION=(\S+)", ln)at line 499 versusre.match(r"\s*ARG CLAUDE_CODE_VERSION=(\S+)", ln)at line 475. The samefor ln in ...splitlines(): m = re.match(r"\s*ARG <NAME>=(\S+)", ln)loop is also at line 526 (UV_VERSION) and line 554 (PI_VERSION again, in test_pi_cli_baked_and_pinned). So the file now has 4 copies, and PI_VERSION is read from docker/Dockerfile by two separate parsers. Fix: add one module-level helper,_arg_pin(df_text: str, name: str) -> str | None, and use it at all four sites. Then replace the two parity tests with one@pytest.mark.parametrize("arg", ["CLAUDE_CODE_VERSION", "PI_VERSION"])test. Keep the Pi-only package-string assert (line 510), or move it into test_pi_cli_baked_and_pinned so that test checks both Dockerfiles. The anchor table puts duplication across 4+ sites at High. I filed this as Medium because the code is test-only, the loop is short, and the PR added only one of the four copies.
Nits
- [Axis 1] test_pi_cli_baked_and_pinned docstring wrongly says the runtime kit does not bake Pi (
tests/test_image_from_dockerfiles.py:548) — Lines 548-549 at PR head still say:Scope: main docker/Dockerfile only — Dockerfile.runtime deliberately does NOT bake Pi (inject-mode is out of scope), so it is not asserted here.The PR now bakes Pi into docker/Dockerfile.runtime (lines 92-95). The new test 40 lines above (line 488, test_pi_version_pin_matches_framework) asserts that the runtime kit installs Pi. So the old docstring contradicts the PR in the same file. This is a stale claim the rename/removal grep (Technique 1) should have caught. Fix: delete or reword the sentence, for example:Scope: main docker/Dockerfile; the runtime kit's Pi pin is guarded by test_pi_version_pin_matches_framework.Better: merge the two Pi tests, as in finding 1. - [Axis 1] A reflowed paragraph in PI.md leaves one 114-character source line in an otherwise ~80-column paragraph (
docs/agents/PI.md:221) — Line 221 at PR head isbrings its own base image runs Pi too.OPENROUTER_API_KEYis forwarded into the container by the docker driver's(114 characters). Lines 218-223 around it wrap at about 75-81 columns. The PR reflowed lines 219-220 but did not reflow the rest of the sentence. Re-wrap lines 221-223 to the same width as the rest of the paragraph. This is cosmetic only and does not change the rendered output. - [Axis 6] The Pi install accepts an unsupported Node engine with only a warning, and the
pi --versiongate cannot catch it (docker/Dockerfile.runtime:93) — The kit pinsARG NODE_VERSION=22.14.0(line 68). The new step at lines 93-95 runsRUN PATH="/opt/coder-eval/node/bin:${PATH}" npm install -g --prefix /opt/coder-eval/node \ @earendil-works/pi-coding-agent@${PI_VERSION} \ && PATH="/opt/coder-eval/node/bin:${PATH}" pi --version.@earendil-works/pi-coding-agent@0.87.1declaresengines = { node: '>=22.19.0' }(checked withnpm view). npm 10.9.2, which ships with Node 22.14.0, only printsnpm warn EBADENGINE ... required: { node: '>=22.19.0' }, current: { node: 'v22.14.0' }and installs anyway. I reproduced this innode:22.14.0-slim:pi --versionprinted0.87.1with exit 0, and api -p --mode jsonrun reached the provider and got a 401. So the build gate stays green when the package runs on a Node version it does not support. The framework image does not have this problem: it installs the newest Node 22.x throughdeb.nodesource.com/setup_22.x. As a result, inject mode runs Pi on a different and unsupported Node than rebase mode, and nothing reports it. I saw no breakage today, which is why this is Low. Fix: make the mismatch fail the build by adding--engine-strictto thisnpm install -g(or setnpm_config_engine_strict=truefor the step), or raiseNODE_VERSIONto at least 22.19.0. A static parity check could also compare the kit's NODE_VERSION with the engine floor of each npm CLI it installs.
What's Missing
Tests:
- 🟡 No CI job or test builds docker/Dockerfile.runtime. The kit is built only by
make coder-eval-runtime, and .github/ does not reference it. So the newpi --versionbuild gate (Dockerfile.runtime:93-95) never runs in this repo. The author says they did not build the image. The only build evidence is the downstream bench build 13944412, which built a local patch and not this commit. The new test is static text matching only. Add a CI smoke build of the kit, or a docker-marked test, that runs/opt/coder-eval/node/bin/pi --versionin the final stage. Otherwise a broken install or a missing bin is found only by consumers who inject the kit. (trigger: docker/Dockerfile.runtime) - 🔵 The Pi image guards are now split across two tests that disagree. test_pi_cli_baked_and_pinned still scopes itself to docker/Dockerfile only and says the kit does not bake Pi. test_pi_version_pin_matches_framework covers the kit's pin and package string, but not the
latestguard. Merge them so one test asserts pin, non-latest and${PI_VERSION}install on both Dockerfiles. (trigger: tests/test_image_from_dockerfiles.py) (restates: Axis 1: test_pi_cli_baked_and_pinned docstring wrongly says the runtime kit does not bake Pi)
Parallel paths:
- 🔵 docker/coder_eval_runtime_entrypoint.sh was not updated. Its header comment (lines 6-9) still says only
coder-eval,claude, andnoderesolve from the kit. The PR updated the matching comments in Dockerfile.runtime (contract line 13 and the entrypoint note near line 113) to addpi, but missed the entrypoint script that those comments describe. (trigger: docker/Dockerfile.runtime) - 🔵 The 'how to bake another agent CLI' guidance still names only the framework image. The docker/Dockerfile comment (lines 81-91, 'Adding it means a pinned version ... as CLAUDE_CODE_VERSION / PI_VERSION do') and docs/agents/OPENCODE.md 'Running in Docker' (line ~292) both name docker/Dockerfile only. After this PR, a baked Node CLI must also go into Dockerfile.runtime with a pin-parity test, or inject-mode tasks break as Pi did. Neither surface says this. (trigger: docker/Dockerfile.runtime)
- 🔵 This PR fixes agent coverage between the two images for Pi only. The kit still installs only
--extra codex(Dockerfile.runtime:63). The framework image addsantigravity,litellmandharbor(Dockerfile:107). So--type antigravityandroute: litellmjudges still fail in inject mode, the same way Pi did before this PR. DOCKER_ISOLATION.md (~line 158) mentions this only as 'core + codex' and has no per-agent rebase-vs-inject support list. Add that list, or a static check that diffs the agent sets of the two images. (trigger: docs/DOCKER_ISOLATION.md) - 🔵 The new Pi pin-parity test leaves Node itself unchecked. The kit pins NODE_VERSION=22.14.0 (line 68). The framework image installs the newest Node 22.x from nodesource. So inject-mode Pi runs on a Node version below the package's
>=22.19.0engine floor, and rebase-mode Pi does not. No parity test or note covers the runtime under the pinned CLI. (trigger: docker/Dockerfile.runtime) (restates: Axis 6: The Pi install accepts an unsupported Node engine with only a warning, and thepi --versiongate cannot catch it)
Harness & Lint Improvements
Static checks (lint / type):
- [ce-lint] CE069 Dockerfile ARG-pin parity. This is a whole-tree
@pytest.mark.lintclass in tests/test_custom_lint.py, because it reads Dockerfiles and not Python ASTs. Parse everyARG <NAME>_VERSION=<value>line in docker/Dockerfile and docker/Dockerfile.runtime with ONE shared module-level helper,_arg_pin(text, name). Fail when a*_VERSIONARG is declared in both files with different values. Also fail when annpm install -g <pkg>@${<NAME>_VERSION}appears in one file but not the other, unless the name is in an EXEMPT dict that gives a reason. This one generic rule replaces the hand-written per-tool tests test_claude_code_version_pin_matches_framework and test_pi_version_pin_matches_framework (tests/test_image_from_dockerfiles.py:463/488). Delete those tests and their copied_pinparsers in the same change, so the next CLI baked into both images gets parity checking without a third copy. Use the next free number after CE068. Prevents: Finding 1: the copied Pi pin-parity test and the 4 copies of the ARG regex loop at tests/test_image_from_dockerfiles.py:473/497/526/554. Finding 2: the stale docstring at :548 is deleted together with the duplicate Pi test, and nothing replaces it with a hand-written scope claim. - [ce-lint] CE070 npm global installs must be engine-strict. This is a whole-tree
@pytest.mark.lintclass over docker/Dockerfile*. EveryRUNthat containsnpm install -gmust pass--engine-strict, or it must run after anENV npm_config_engine_strict=truein the same stage. Then an npm package whoseengines.nodefloor is above the installed Node fails the image build, and does not just printnpm warn EBADENGINEand continue. The rule also flags the existing unflagged installs at docker/Dockerfile:37 (claude-code) and :43 (pi-coding-agent). Fix those in the same change and do not exempt them. Prevents: Finding 4: docker/Dockerfile.runtime:93 installs @earendil-works/pi-coding-agent@0.87.1 (engines node>=22.19.0) on the pinned NODE_VERSION=22.14.0. The build passes, and thepi --versiongate cannot detect the problem.
Harness improvements (not statically reachable):
- Add a network-gated check of the Node engine floor, as a
maketarget run in the image-build CI job or marked live. For eachnpm install -g <pkg>@${X_VERSION}in docker/Dockerfile.runtime, runnpm view <pkg>@<pin> engines.nodeand assert that the kit'sARG NODE_VERSIONsatisfies the range. Run it when docker/Dockerfile.runtime or a*_VERSIONpin changes. Why not static: The engine floor is in the npm registry metadata of the pinned version, not in the repo. A static rule can require--engine-strict(CE070), but it cannot compute the floor without a network call. This check reports the mismatch at pin-bump time, before a docker build. Prevents: Finding 4: inject mode runs Pi on an unsupported Node (22.14.0 < 22.19.0), but rebase mode uses the newest nodesource 22.x, and nothing reports the difference. - Make no static check for stale scope claims in docstrings. Instead, the code-review Technique 1 (rename/removal grep) must also grep tests/ for the negated form of the change, e.g.
does NOT bake <tool>/not installed in <file>, when a PR adds a tool to an image or file that it did not have before. Why not static: To detect that a prose sentence ('Dockerfile.runtime deliberately does NOT bake Pi') contradicts the new state of another file, a tool must understand the sentence. No grep or AST pattern separates a stale claim from a true one. CE069 removes this instance, because it deletes the test that has the docstring. Prevents: Finding 2: the stale docstring at tests/test_image_from_dockerfiles.py:548. - Decision recorded: add no line-width rule for docs/*.md. The repo has about 272 non-table, non-URL source lines in docs/ longer than 100 columns, so a width cap would either fail a large part of the tree or need a high limit that would not catch a 114-column line. The rendered output does not change, so leave this cosmetic issue to review. Why not static: A width rule is possible, but it is not worth the noise. Markdown source wrapping does not change the rendered HTML, and the existing docs have no shared wrap width that a rule could enforce without a large reflow. Prevents: Finding 3: the 114-character line in a ~80-column paragraph at docs/agents/PI.md:221 (deliberately not prevented).
Top 5 Priority Actions
- No finding changes a task's score or final_status for the same agent output; the highest-impact item is docker/Dockerfile.runtime:93: add
--engine-strictto the Pinpm install -g(or raiseARG NODE_VERSIONat line 68 from 22.14.0 to at least 22.19.0) so that the build fails when Pi 0.87.1 (engines node >=22.19.0) would otherwise run on an unsupported Node in inject mode. - Add a static parity test that compares the runtime kit's NODE_VERSION (docker/Dockerfile.runtime:68) with the
engines.nodefloor of each npm CLI it installs, so that a later CLI bump cannot quietly make inject mode run on a different Node than rebase mode. - Remove the duplicated parsing in tests/test_image_from_dockerfiles.py: add one module-level
_arg_pin(df_text, name)helper, use it at the four copied ARG-pin parsing sites (lines 475, 499, 526, 554), and merge the Claude and Pi pin-parity tests (lines 473 and 497) into one@pytest.mark.parametrize("arg", ["CLAUDE_CODE_VERSION", "PI_VERSION"])test. - Fix the stale docstring at tests/test_image_from_dockerfiles.py:548, which says that Dockerfile.runtime does not bake Pi; that is now wrong because the PR adds Pi at docker/Dockerfile.runtime:92-95. The simplest fix is to move the Pi package-string assert (line 510) into test_pi_cli_baked_and_pinned so that this test checks both Dockerfiles.
- Re-wrap docs/agents/PI.md:221-223 to the ~80-column width of the paragraph around it (line 221 is 114 characters); this is a cosmetic change only and does not change the rendered output.
Stats: 0 🔴 · 0 🟠 · 1 🟡 · 3 🔵 across 8 axes reviewed.

Problem
docker/Dockerfileinstalls the Pi CLI atPI_VERSION, andPI.mddocuments--driver docker --type pias supported.docker/Dockerfile.runtimedoes not install it. A task that brings its own base image (runtime-mode: inject) gets onlynodeandclaudefrom the kit, so every Pi attempt there fails with "The 'pi' CLI was not found on PATH".Change
docker/Dockerfile.runtimeinstalls@earendil-works/pi-coding-agent@${PI_VERSION}into/opt/coder-eval/node, the prefix Claude Code already uses. The entrypoint already puts that bin directory onPATH.pi --versionruns in the builder, so a bin that did not land fails the build instead of every task.PI_VERSIONis 0.87.1, the same asdocker/Dockerfile.test_pi_version_pin_matches_frameworkkeeps the two pins equal, the waytest_claude_code_version_pin_matches_frameworkdoes for Claude Code.PI.mdandDOCKER_ISOLATION.mdsay the kit carries Pi.No other files change.
OPENROUTER_API_KEYis already in the docker driver'senv_passthrough.Evidence
devtools-coding-agent-bench injects the kit into every task image. It carried this same hunk as a local patch on 0.12.12, and build 13944412 built the patched kit. In an injected task image, Pi then started and called OpenRouter for three models. Those attempts ended on our account's 401, not on the image. Once a release carries this change, we drop the patch.
Tests
ruff checkandruff format --checkpass. The runtime-kit tests intests/test_image_from_dockerfiles.pypass, both the new one and the existing Claude Code pin and install-dir tests. I did not build the image locally.🤖 Generated with Claude Code
https://claude.ai/code/session_01RarvxSsr4K2KkBisTbXxQY