diff --git a/docker/Dockerfile.runtime b/docker/Dockerfile.runtime index 9b9eba30..373fdbb9 100644 --- a/docker/Dockerfile.runtime +++ b/docker/Dockerfile.runtime @@ -10,7 +10,7 @@ # # Contract (must match convert_skillsbench_task.py's inject block + the host): # /opt/coder-eval/venv/bin/coder-eval the CLI (its own Python) -# /opt/coder-eval/node/bin/{node,claude} Node + the Claude Code CLI +# /opt/coder-eval/node/bin/{node,claude,pi} Node + the Claude Code and Pi CLIs # /usr/local/bin/coder_eval_entrypoint.sh entrypoint (host pins --entrypoint here) # LABEL org.coder-eval.version= for `docker inspect` / parity # @@ -83,6 +83,17 @@ RUN mkdir -p /opt/coder-eval/node \ && PATH="/opt/coder-eval/node/bin:${PATH}" npm install -g --prefix /opt/coder-eval/node \ @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} +# --- 4. The Pi CLI, under the kit dir. -------------------------------------- +# Same package and pin as docker/Dockerfile, so `--driver docker --type pi` runs +# in an injected task image as it does in a rebased one; a parity test +# (tests/test_image_from_dockerfiles.py) keeps the two PI_VERSION pins equal. +# `pi --version` fails the build here if the package's bin did not land on the +# kit's PATH, instead of every pi task failing later with "pi CLI not found". +ARG PI_VERSION=0.87.1 +RUN 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 + # Sanity: the CLI runs from the kit (no reliance on a system Python). Run it in # the builder, where the kit lives; the final stage is a bare carrier. RUN /opt/coder-eval/venv/bin/coder-eval _run-task-internal --help > /dev/null @@ -99,7 +110,7 @@ COPY --from=builder /opt/coder-eval /opt/coder-eval # Entrypoint: puts the kit's bins on PATH, then runs the orchestrator. Same # filename + location as the framework image, so the host's fixed # `--entrypoint /usr/local/bin/coder_eval_entrypoint.sh` works for injected -# images too — coder-eval/claude/node all resolve from the kit regardless of what +# images too — coder-eval/claude/pi/node all resolve from the kit regardless of what # the task's base image provides. COPY docker/coder_eval_runtime_entrypoint.sh /usr/local/bin/coder_eval_entrypoint.sh RUN chmod +x /usr/local/bin/coder_eval_entrypoint.sh diff --git a/docs/DOCKER_ISOLATION.md b/docs/DOCKER_ISOLATION.md index 8c6e6383..bf6f0772 100644 --- a/docs/DOCKER_ISOLATION.md +++ b/docs/DOCKER_ISOLATION.md @@ -145,7 +145,7 @@ LABEL org.coder-eval.version="" ``` `make coder-eval-runtime` builds the kit (`docker/Dockerfile.runtime`): a -standalone CPython + Node + the `coder-eval` CLI + Claude Code, all under +standalone CPython + Node + the `coder-eval` CLI + Claude Code + Pi, all under `/opt/coder-eval`, plus the entrypoint at the **same** `/usr/local/bin/...` path the host pins. The kit is **glibc-only** — it runs on debian/ubuntu/fedora/rhel/… but not musl/Alpine. diff --git a/docs/agents/PI.md b/docs/agents/PI.md index eb515197..ca9fe754 100644 --- a/docs/agents/PI.md +++ b/docs/agents/PI.md @@ -216,8 +216,9 @@ success — the error names the unrecognized event types it saw. Intentional cut ## Running in Docker Pi **is supported under `--driver docker`.** The pinned Pi CLI is baked into -`docker/Dockerfile` (`ARG PI_VERSION`, alongside the claude-code CLI), and -`OPENROUTER_API_KEY` is forwarded into the container by the docker driver's +`docker/Dockerfile` (`ARG PI_VERSION`, alongside the claude-code CLI) and, at the +same pin, into the runtime kit (`docker/Dockerfile.runtime`), so a task that +brings its own base image runs Pi too. `OPENROUTER_API_KEY` is forwarded into the container by the docker driver's default `env_passthrough` allowlist — so `--driver docker --type pi` runs end-to-end with no per-task `env_passthrough_extra`: diff --git a/tests/test_image_from_dockerfiles.py b/tests/test_image_from_dockerfiles.py index 4f96aff9..2f35546a 100644 --- a/tests/test_image_from_dockerfiles.py +++ b/tests/test_image_from_dockerfiles.py @@ -485,6 +485,33 @@ def _pin(df: Path) -> str | None: ) +def test_pi_version_pin_matches_framework() -> None: + """The runtime kit must install Pi at the framework image's PI_VERSION — + else `--type pi` would find no CLI in an injected task image, or run a + different Pi than a rebased one.""" + root = Path(__file__).resolve().parents[1] + runtime_df = _runtime_dockerfile() + if runtime_df is None: + pytest.skip("docker/Dockerfile.runtime not present") + + def _pin(df: Path) -> str | None: + for ln in df.read_text(encoding="utf-8").splitlines(): + m = re.match(r"\s*ARG PI_VERSION=(\S+)", ln) + if m: + return m.group(1) + return None + + fw, kit = _pin(root / "docker" / "Dockerfile"), _pin(runtime_df) + assert fw and kit, f"PI_VERSION ARG missing (framework={fw!r}, kit={kit!r})" + assert fw == kit, ( + f"PI_VERSION drift: docker/Dockerfile pins {fw!r} but docker/Dockerfile.runtime pins {kit!r} — " + "inject and rebase tasks would run different Pi versions." + ) + assert "@earendil-works/pi-coding-agent@${PI_VERSION}" in runtime_df.read_text(encoding="utf-8"), ( + "docker/Dockerfile.runtime must install @earendil-works/pi-coding-agent at ${PI_VERSION}." + ) + + def test_uv_pinned_in_agent_image() -> None: """docker/Dockerfile must install a PINNED uv through the versioned installer URL. Agents run `uv` themselves, so its install speed is part of