Conversation
…x slower The agent image installed uv from the unversioned installer, so each rebuild shipped whatever uv was latest. The 0.12.1 image (2026-09-12) moved from uv 0.12.9 to 0.12.12. Agents run uv themselves (coded-agent and function tasks build a venv in the bind-mounted workspace), and uv's cache is in the image, so uv cannot hardlink and copies every file. From 0.12.11 that copy is about 40x slower. Measured with a warm cache, a bind mount, and `uv pip install uipath-langchain` (146 packages): - 0.12.9 / 0.12.10: 0.3s - 0.12.11 / 0.12.12 / 0.12.19 / 0.12.21: 10-14s - same filesystem (cache and venv both in /tmp): 0.2s on every version - published coder-eval-agent:0.12.8 (uv 0.12.19): 10.6s; this image: 0.2-0.3s On the CI host the same install went from about 4s (nightlies up to 09-11) to 60-130s (09-14 onward). Two installs per coded-agent task added ~2 min of wall clock. Codex's exec timeout killed some of those installs (exit 130), and the agents' retries showed up as extra uip/uv commands. Pin uv to 0.12.10 (the last good release) through `ARG UV_VERSION` and the versioned installer URL, the same way the Claude Code and Pi CLIs above are pinned. `test_uv_pinned_in_agent_image` fails on an unversioned installer or a missing or non-exact pin; verified failing against the old Dockerfile. 🤖 Generated with Claude Code Co-Authored-By: [Claude](mailto:noreply@anthropic.com) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MzgU89VtXESbzJfAAu9ay
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:209 (2 files) axis:1,2,3,4,5,6,7,8
Scope: pr:209 (2 files) axis:1,2,3,4,5,6,7,8 · branch fix/pin-uv-agent-image (PR #209 by @tmatup → main, OPEN) · 4e0aff9 · 2026-10-01T04:42Z · workflow variant
Change class: simple — pins the uv version in the agent Dockerfile via an ARG and adds a static text-match test; no control flow, schema, or API change
The repo is healthy: seven of eight axes score 10/10 (Code Quality & Style is weakest at 9.4) and no confirmed finding can change a task's score or final_status for identical agent output, so the real risks are only a uv install step that fails silently and gives a misleading build error, plus small duplication and history-comment drift in the uv-pin change; the bottom line is to merge after the short fixes below.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.4 / 10 | 0 | 0 | 1 | 1 | Third copy of the inline ARG <NAME>= pin-parsing loop; the sibling _pin helper was not lifted and reused |
| 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 | A failed versioned uv download makes the install layer exit 0 and the build later fails with "uv: not found" |
| 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.4 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 1 · 🔵 2 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
- [Axis 1] Third copy of the inline
ARG <NAME>=pin-parsing loop; the sibling_pinhelper was not lifted and reused (tests/test_image_from_dockerfiles.py:495-502) — The new test copies the PI_VERSION loop line for line. Lines 495-502 aredf_text = (Path(__file__).resolve().parents[1] / "docker" / "Dockerfile").read_text(encoding="utf-8")/pin: str | None = None/for ln in df_text.splitlines():/m = re.match(r"\s*ARG UV_VERSION=(\S+)", ln)/if m: pin = m.group(1); break. The same loop is at 523-530 (ARG PI_VERSION=), and a nested_pinhelper does the same work at 473-478 (ARG CLAUDE_CODE_VERSION=). That makes three sites of the same pin extraction, and two sites of the framework-Dockerfile path expression (495, 523). This matches the Axis 1 anchor 'duplication across 2–3 sites' (Medium). Move_pinto module level as_arg_pin(df_text: str, name: str) -> str | None, next to the existing_runtime_dockerfile()helper at line 416. Add a_framework_dockerfile_text()helper too. Then each pin test is about 3 lines, and the next pinned tool does not add a fourth copy.
Nits
- [Axis 1] The uv pin comment in the Dockerfile holds history and benchmark notes that the test docstring repeats (
docker/Dockerfile:45-54) — The new comment is a 10-line run. It records history that belongs in git or .claude/notes/, not next to the code:install: 0.3s on 0.12.10, 10-14s on 0.12.11/.12/.19/.21; 60-130s on the CI/host), and the unpinned install picked it up silently in the 0.12.1 image/(2026-09-12, uv 0.12.12). The CLAUDE.md principle says 'what it used to be belongs in git'. Also, '0.12.1 image' means coder_eval release v0.12.1 (CHANGELOG.md:898), but it sits between uv versions 0.12.10/0.12.11/0.12.12, so a reader can easily take it as a uv version. Reduce the comment to the contract: why uv is pinned (cross-filesystem copy into the bind-mounted workspace is about 40x slower from 0.12.11), and that a bump requires timing an install into a bind-mounted dir. Move the benchmark table and the incident date to a .claude/notes/ entry or the commit message. If a version reference must stay, write 'coder_eval v0.12.1'. The test docstring at tests/test_image_from_dockerfiles.py:489-494 has the same history ('moved the image from uv 0.12.9 to 0.12.12 on a rebuild'). Reduce it to the contract stated in its last sentence. - [Axis 6] A failed versioned uv download makes the install layer exit 0 and the build later fails with "uv: not found" (
docker/Dockerfile:56) — Line 56RUN curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | env UV_INSTALL_DIR=/usr/local/bin shruns under Docker's default/bin/sh -c, which has no pipefail. The pipeline's exit status is that ofsh. If curl fails,-fexits 22 and writes nothing,shreads an empty script, and the step exits 0. The pin adds a new way for curl to fail: a mistyped or withdrawn version on a deliberate bump returns 404. A transient astral.sh 5xx or network blip causes the same result. The step is then reported as successful (and BuildKit can cache it as successful). The build only fails two steps later at line 107 (uv export --frozen ... | uv pip install ...) with "uv: not found", which hides the real cause: the pinned version could not be fetched. Make the failure happen at the step that caused it. AddSHELL ["/bin/bash", "-o", "pipefail", "-c"]before this RUN, or download the script first (curl -LsSf -o /tmp/uv-install.sh https://astral.sh/uv/${UV_VERSION}/install.sh && env UV_INSTALL_DIR=/usr/local/bin sh /tmp/uv-install.sh). Then append&& uv --version | grep -F "${UV_VERSION}"so the layer also confirms that the pinned version is the one installed. This bug existed before the PR, with the unversioned URL, but this PR edits the line and adds the 404 case. Severity is Low because the build still fails, only with the wrong cause and at the wrong step. n/a
What's Missing
Nightly pipeline:
- 🟡 The PR gives the CI-host speedup (60-130s back to about 4s per install), but it does not say what happens to the nightly. (a) The fix reaches the nightly only after release.yml publishes a new coder-eval-agent: tag and the nightly moves to that tag. Until then, nightlies on the published 0.12.8/0.12.9 images (uv 0.12.19) stay slow. (b) Runs from 09-14 until the new image are a different cohort. The commit message says Codex's exec timeout killed uv installs (exit 130) and the retries added commands. So pass rate, turn count, command count and wall clock on coded-agent/function tasks all move at this boundary, and not only duration. State the rollout path, and add an annotation to the evalboard trend at the image switch, so that no one reads the step change as an agent or model change. (trigger: docker/Dockerfile)
Parallel paths:
- 🟡 The run records do not capture the uv version. The CLAUDE_CODE_VERSION pin is visible in every run as environment_info.claude_code_cli (src/coder_eval/utils.py:480), and docs/agents/CLAUDE_CODE.md tells consumers to segment on it. The new UV_VERSION pin has no matching field. This is why the 0.12.1 image's silent move to uv 0.12.12 went unattributed for about two weeks of nightlies. Record
uv --versionin environment_info next to claude_code_cli, so that the next uv bump can be seen in run.json and in evalboard cohorts. (trigger: docker/Dockerfile) - 🔵 After this PR, the uv that writes uv.lock and the uv that reads it are different versions. CI (
pip install uv, pr-checks.yml:88-91, setup-uv:265) and release.yml's prereleaseuv lock(line ~194) use the latest uv. The image'suv export --frozen(docker/Dockerfile:107) now runs on the frozen 0.12.10. If a later uv writes a lock revision that 0.12.10 cannot read, the image build fails on the release path. No signal tells anyone to bump the pin (dependabot does not track Dockerfile ARGs, and there is no tracking issue for a fixed upstream uv). Pin the CI/release uv to the same version, or add a guard and a tracking item. (trigger: docker/Dockerfile) - 🔵 docker/Dockerfile.runtime:38 still uses the unversioned installer. The PR body explains that the speed issue does not apply, because uv is only in the builder stage. But that uv also runs
uv python install ${PYTHON_VERSION}with PYTHON_VERSION=3.13, and each uv release bundles its own python-build-standalone list. So the runtime kit's CPython patch version still changes when uv changes on a rebuild. Also, the comment on line 36 ('same tool the main image and host sandbox use') now hides a version difference. Pin it to the same UV_VERSION, or record in the Dockerfile comment why patch drift is acceptable. (trigger: docker/Dockerfile) - 🔵 Two other paths can hit the same cross-filesystem slowdown, and nothing tells users. (1) Custom and BYO task images (the docs/DOCKER_ISOLATION.md custom-image and runtime-inject sections) that install their own unpinned uv and build a venv in the bind-mounted workspace. (2) The host driver: local path in src/coder_eval/sandbox.py:808-840, which uses whatever uv is on the host, with the cache on a different filesystem from the sandbox. DOCKER_ISOLATION.md:18 describes uv as part of the 'pinned toolchain' but gives no guidance for images that bring their own uv. (trigger: docker/Dockerfile)
Tests:
- 🔵 test_uv_pinned_in_agent_image only checks the Dockerfile text. No check confirms that the built image actually contains uv == UV_VERSION (for example, a
uv --versionassert in the docker-publish/release build or the image smoke test). Such a check would catch a failed versioned download or an installer that ignores the version. (trigger: tests/test_image_from_dockerfiles.py) (restates: Axis 6: A failed versioned uv download makes the install layer exit 0 and the build later fails with "uv: not found") - 🔵 The comment's bump rule ('Bump deliberately, after timing an install into a bind-mounted dir') is a process note only. No benchmark, make target or opt-in test times a warm-cache
uv pip installinto a bind mount against a threshold. The next person who bumps UV_VERSION has no tool to do the timing, so the same regression can come back on a deliberate bump. (trigger: docker/Dockerfile)
Harness & Lint Improvements
Static checks (lint / type):
- [ce-lint] CE067 (Dockerfile pipefail): any
RUNin docker/Dockerfile* that pipes into a shell or another command (a single|, not||) must sit after aSHELL ["/bin/bash", "-o", "pipefail", "-c"]in the same build stage, or must start withset -o pipefail. This is the same check as hadolint DL4006. Dockerfiles are not Python ASTs, so add it as a stdlib text-scan@pytest.mark.lintclass in tests/test_custom_lint.py (the whole-tree / doc-surface slot), not in tests/lint/runner.py. Note: a pipefail-only fix still letscurl -ffail with no output,shread an empty script and exit 0. So the rule should also accept the download-to-file form (curl -o f && sh f) as compliant. Prevents: Finding 3 (docker/Dockerfile:56, a 404 or 5xx on the versioned uv download exits 0 and the build later fails withuv: not found). It also flags the same unchecked pipes that exist today at docker/Dockerfile:34 (nodesourcecurl | bash) and docker/Dockerfile.runtime:38 (uvcurl | sh). - [ce-lint] CE068 (Dockerfile tool pins, table-driven): one rule over every
ARG <NAME>_VERSION=<v>in docker/Dockerfile and docker/Dockerfile.runtime. It asserts that (a) the value is notlatestor empty, (b) at least one RUN line references${<NAME>_VERSION}, so the install cannot drift from the pin, (c) an ARG name declared in both Dockerfiles has the same value in both, and (d) a known installer URL or package (astral.sh/uv/install.sh,npm install -g <pkg>) is never fetched without a version segment. It uses one module-level_arg_pins(df_text) -> dict[str, str]parser. Delete before you guard: this rule replaces the three hand-written per-tool tests in tests/test_image_from_dockerfiles.py (test_claude_code_version_pin_matches_framework, test_pi_cli_baked_and_pinned, test_uv_pinned_in_agent_image), so the duplicated ARG-parsing loop goes away and does not get a shared helper. Add it as a@pytest.mark.lintclass in tests/test_custom_lint.py, or keep it in tests/test_image_from_dockerfiles.py as one parametrized test. A fourth pinned tool then needs no new code. Prevents: Finding 1 (the third copy of the inlineARG <NAME>=pin loop at tests/test_image_from_dockerfiles.py:495-502, 523-530 and 473-478, plus the second copy of the framework-Dockerfile path expression). Check (d), and check (c) for any shared ARG, also flag an unpinned uv install that remains in the parallel docker/Dockerfile.runtime:38, which the PR may not have pinned. The same would apply to any later tool added to only one image. - [ce-lint] Extend
make docs-budget(tests/lint/prose_budget.py_ROOTS, which today is onlysrc/coder_evalandtests) to scan the#comment runs indocker/Dockerfile*anddocker/*.shwith the existing_COMMENT_RUN_LINES = 8run cap and_RUN_BLANK_BRIDGE. Dockerfile comments are#own-line comments, so the existing run logic applies unchanged and only needs a non-tokenize line reader for non-Python files. The 'history versus contract' judgement (benchmark numbers, incident dates, an ambiguous0.12.1 image) is semantic and stays a review item. The run cap enforces the shape, which pushes the paragraph into .claude/notes/ behind aRationale:pointer. Prevents: Finding 2 (the 10-line uv pin comment at docker/Dockerfile:45-54 is over the 8-line cap but was not checked, because docker/ is outside the budget's roots). The test-docstring half of finding 2 (tests/test_image_from_dockerfiles.py:489-494) is under the 150-word docstring cap, so only review can catch it. If CE068 replaces that test, the docstring is deleted with it.
Harness improvements (not statically reachable):
- Add a post-build image-contract check (a
make docker-image-verifytarget, run in docker-publish.yml and in pr-checks.yml when docker/** changes). For eachARG <NAME>_VERSIONthat CE068 parses, it runs the tool's--versioninside the built coder-eval-agent and coder-eval-runtime images and asserts that the output contains the pinned value. This is the generic form of the&& uv --version | grep -F "${UV_VERSION}"that finding 3 recommends. Why not static: Only a built image shows which binary the installer actually put on PATH. A static scan can confirm that the URL contains the version, but it cannot see an installer that ignores it, redirects to latest, or fails silently. Prevents: Finding 3 (the build reports success, or fails at the wrong step, when the pinned uv version cannot be fetched), and any drift between a pinned ARG and the installed binary. - Record the uv bind-mount install benchmark (0.3s on 0.12.10 versus 10-14s on 0.12.11+, and 60-130s on CI) and the incident in a .claude/notes/ entry. Add a
make docker-uv-benchtarget that times auv pip installof the project lockfile into a bind-mounted directory in the agent image. The Dockerfile comment then states only the contract ('a uv bump requires running make docker-uv-bench') plus aRationale:pointer. Why not static: The regression is a cross-filesystem copy-performance cliff. It only appears at runtime on a real bind mount on the CI host, so no source pattern shows it. Prevents: Finding 2 (history and benchmark data inline in docker/Dockerfile:45-54), and an unbenchmarked future uv bump bringing back the 40x slowdown that the pin exists to prevent.
Top 5 Priority Actions
- No finding can change a task's score or final_status for identical agent output, so start with the build risk: at docker/Dockerfile:56, add
SHELL ["/bin/bash", "-o", "pipefail", "-c"]before the line (or download install.sh to a file first) and append&& uv --version | grep -F "${UV_VERSION}", so a 404 or network failure on the pinned uv makes this step fail instead of the later 'uv: not found' at docker/Dockerfile:107. - At tests/test_image_from_dockerfiles.py:495-502 (also 523-530 and the nested
_pinat 473-478), move the pin parsing to a module-level_arg_pin(df_text, name) -> str | Noneand add_framework_dockerfile_text()next to_runtime_dockerfile()at line 416, so that the three copies become one and the next pinned tool does not add a fourth. - At docker/Dockerfile:45-54, cut the 10-line comment down to the contract (uv is pinned because the copy into the bind-mounted workspace is about 40x slower from 0.12.11, and a bump must time an install into a bind-mounted dir), and move the benchmark timings and the 2026-09-12 incident to a .claude/notes/ entry or the commit message.
- At tests/test_image_from_dockerfiles.py:489-494, remove the history from the test docstring ('moved the image from uv 0.12.9 to 0.12.12 on a rebuild') and keep only the contract in its last sentence; where a release must be named, write 'coder_eval v0.12.1' so that no one reads it as a uv version.
- Add a test or a .claude/harness-candidates.md entry that fails when a Dockerfile
RUN curl ... | shruns without pipefail, so this class of silent install failure (the root cause at docker/Dockerfile:56, which was there before the PR) cannot come back on another pinned tool.
Stats: 0 🔴 · 0 🟠 · 1 🟡 · 2 🔵 across 8 axes reviewed.
uipreliga
left a comment
There was a problem hiding this comment.
fix what you agree with and 🚢

Summary
Short version: the agent image installed uv unpinned, and uv 0.12.11+ makes the cross-filesystem copy (cache in the image, venv on the bind-mounted workspace) about 40× slower. Coded-agent tasks have paid an extra ~2 min of wall clock since the 0.12.1 image (2026-09-12). This PR pins uv to 0.12.10, the last good release.
docker/Dockerfile:ARG UV_VERSION=0.12.10+https://astral.sh/uv/${UV_VERSION}/install.sh, with the reason in a comment. Same pattern as the Claude Code and Pi pins in that file.tests/test_image_from_dockerfiles.py:test_uv_pinned_in_agent_imagefails on an unversioned installer URL or a missing or non-exact pin.Evidence
CI,
skill-flow-coded-agent(UiPath/skills nightlies + adhoc v1/v2 runs):environment_info)Installed 146 packages in …The package set is unchanged (145–146). Resolve and download take under a second (
Prepared 118 packages in 815ms). All the time is in the link step afterwarning: Failed to hardlink files; falling back to full copy. In adhoc-2026-09-30_19-54-19, one scaffold command spentInstalled 146 packages in 1m 05splusInstalled 31 packages in 54.08s. Codex's exec timeout killed four such installs (exit 130), and the agents' retries showed up as extrauip/uvcommands.Local repro (Docker, bind mount, warm cache, fresh venv,
uv pip install uipath-langchain):On the same filesystem it is 0.2s on every version.
strace -cshows the same syscall counts, butopenattime goes from 3.3s to 11.5s. The 0.12.11 release notes touch this path (astral-sh/uv#21468, #21478).This image vs the published one (same bind mount):
ghcr.io/uipath/coder-eval-agent:0.12.8(uv 0.12.19): 10.60s, 10.68sWhy a pin and not
UV_LINK_MODE=symlinkSymlink mode is fast (0.05s), but it leaves
.venvpointing into the container's cache. Detached grading runs in a second container, and the artifact upload reads the workspace after the container exits. Both would see dangling links. The pin restores the behavior every run had before 09-12. BumpUV_VERSIONdeliberately, after timing an install into a bind-mounted dir.Scope notes
docker/Dockerfile.runtimealso uses the unversioned installer. I left it alone: there, uv lives only in thebuilderstage (/usr/local/bin), and only/opt/coder-evalis copied into the final image. That uv never reaches a task image.tests/.coder-eval-versionin UiPath/skills (currently0.12.8) has to move to the release this PR cuts. Both the nightly (daily.sh) andrun-coder-eval.ymlresolve the agent image tag from that file.Test plan
pytest tests/test_image_from_dockerfiles.py: 30 passeddocker/Dockerfileruff check/ruff format --checkdocker build -f docker/Dockerfile→uv --version= 0.12.10; timed install above🤖 Generated with Claude Code
Co-Authored-By: Claude
🤖 Generated with Claude Code
https://claude.ai/code/session_015MzgU89VtXESbzJfAAu9ay