`clawmates-runtime` shipped with `gcc` and `make` but no `cmake`, no `g++` and no `python3-dev`. Measured on clawhdf5, three probes: no cmake → "is `cmake` not installed?" exit 101 after 13s no python3-dev → "cannot find -lpython3.11" exit 101 at link with both → cargo test PASSES exit 0 after 69s This is not only the delivery gate. The AGENTS run in this image, so a coding phase was writing Rust it had no way to compile or test — which reframes the last run's 11 agent commits as unverifiable by construction. `images/agent-toolchain/Dockerfile` (the microVM path) has had `cmake build-essential` all along, and its own header warns about precisely this: "if `cargo` is present in one image and absent in another, the same mission passes or fails depending on which backend it landed on, and nothing says why." Both images now install the same set — it was missing `python3-dev` too. `images/runtime-toolchain.Dockerfile` is a thin local overlay so the laptop can run today without recompiling zeroclaw from the fork; it is meant to be deleted once a runtime image built from the corrected deploy/ Dockerfile is published. Also: a build failure is no longer reported as a red suite. Both are cargo exit 101, and `verify_tests` mapped every non-zero to `Failed(code)` — so a missing toolchain was recorded as the USER's tests failing. It now returns `CouldNotRun` with the reason when the output shows a compile or link failure. Deliberately narrow: a failing `assert!` still reads as red, because letting broken code past `on_green_tests` is the expensive direction to be wrong in. Both directions are pinned by tests built from today's two real samples. And the coding phase finally has a loop: `research_and_code.toml` declared `loop = "until_no_more_int_items"`, which `phase_config.rs` lists as DECLARED_BUT_UNREAD. Iteration is driven by `max_iterations` + `done_when`, and with `max_iterations = 1` and no `done_when` the phase ran ONCE and was never judged — reporting `completed` whatever it produced. Now 3 passes against a stated goal, wording per the measured rule (say what the tree must CONTAIN). Co-Authored-By: Claude Opus 5 <[email protected]>
127 lines
6.6 KiB
Docker
127 lines
6.6 KiB
Docker
# The shared mission toolchain: everything a mission phase needs that is NOT
|
|
# the agent CLI itself. The per-CLI images (agent-claude / agent-kimi /
|
|
# agent-glm, plan A6) are FROM this, so the toolchain facts live in one place.
|
|
#
|
|
# Why a base image rather than three self-contained Dockerfiles: this layer is
|
|
# ~3 GB and ~15 minutes to build. Copying it into each per-CLI image would mean
|
|
# three builds, three copies on disk, and — the actual risk — three places for
|
|
# the scanner and toolchain versions to drift apart. A mission's `done_when`
|
|
# evaluator runs the project's own suite to verify a claim rather than believe
|
|
# it; if `cargo` is present in one image and absent in another, the same mission
|
|
# passes or fails depending on which backend it landed on, and nothing says why.
|
|
#
|
|
# The contents are the ones proven in production by
|
|
# `deploy/clawmates-runtime/Dockerfile`, minus the zeroclaw daemon. A microVM
|
|
# mission runs the direct-session model — `claude -p` exec'd over vsock, see
|
|
# `session_executor::run_session` — so there is no daemon to host and no
|
|
# gateway port to expose. Layer order is copied from that Dockerfile too:
|
|
# cheapest-and-most-stable first, so a version bump low down does not
|
|
# invalidate the expensive layers above it.
|
|
#
|
|
# NOT in `AGENT_IMAGES` in scripts/deploy.sh, deliberately: at this size a
|
|
# `docker save | load` to every fleet node on each deploy would dominate the
|
|
# deploy, and only nodes that actually run microVMs need it. Build it on the
|
|
# node that will run it:
|
|
#
|
|
# ssh osobh@tank "cd ~/clawmates && \
|
|
# docker build -f images/agent-toolchain/Dockerfile -t clawmates/agent-toolchain:dev images/agent-toolchain/"
|
|
FROM debian:bookworm-slim
|
|
|
|
# Node 22 (Kimi Code needs >= 22.19; Claude Code is fine on it) is the runtime
|
|
# for every agent CLI, so it belongs to the shared base rather than to any one
|
|
# of them.
|
|
#
|
|
# `iproute2` is load-bearing on the microVM path, not a convenience. The guest
|
|
# has no network interface at all; its only route out is the agent's proxy on
|
|
# 127.0.0.1, and the guest's `lo` starts DOWN. While it is down a listener on
|
|
# loopback *binds successfully* and then refuses every connection with
|
|
# ENETUNREACH — so without `ip link set lo up` the VM has no egress and nothing
|
|
# says so. Nothing else in the image needs it.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
ca-certificates curl gnupg git jq less procps iproute2 \
|
|
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|
&& apt-get install -y --no-install-recommends nodejs \
|
|
&& npm cache clean --force \
|
|
&& rm -rf /var/lib/apt/lists/* /root/.npm \
|
|
&& node -v && git --version
|
|
|
|
# Upstream Gitea SDLC tooling for agents that operate on git.redclaw.dev.
|
|
# `tea` covers shell-level ops (clone/push, PR checkout); `gitea-mcp` gives the
|
|
# LLM a structured tool surface. Both official upstream binaries.
|
|
ARG TEA_VERSION=0.14.2
|
|
ARG GITEA_MCP_VERSION=1.3.0
|
|
RUN set -eux; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in \
|
|
amd64) mcp_asset="Linux_x86_64" ;; \
|
|
arm64) mcp_asset="Linux_arm64" ;; \
|
|
*) echo "unsupported arch: $arch"; exit 1 ;; \
|
|
esac; \
|
|
curl -fsSL "https://dl.gitea.com/tea/${TEA_VERSION}/tea-${TEA_VERSION}-linux-${arch}" \
|
|
-o /usr/local/bin/tea && chmod +x /usr/local/bin/tea; \
|
|
tmp="$(mktemp -d)" && \
|
|
curl -fsSL "https://gitea.com/gitea/gitea-mcp/releases/download/v${GITEA_MCP_VERSION}/gitea-mcp_${mcp_asset}.tar.gz" \
|
|
-o "$tmp/gitea-mcp.tgz" && \
|
|
tar -xzf "$tmp/gitea-mcp.tgz" -C "$tmp" && \
|
|
install -m 0755 "$tmp/gitea-mcp" /usr/local/bin/gitea-mcp && \
|
|
rm -rf "$tmp"; \
|
|
tea --version | head -1
|
|
|
|
# Security scanners. `security_scan.rs` shells out to all four; when they were
|
|
# missing, every scan produced `<tool>:tool_error` rows instead of findings —
|
|
# a scan that scanned nothing and reported cleanly.
|
|
ARG GITLEAKS_VERSION=8.30.1
|
|
ARG TRIVY_VERSION=0.72.0
|
|
RUN set -eux; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in \
|
|
amd64) gl_arch=x64; tv_arch=64bit ;; \
|
|
arm64) gl_arch=arm64; tv_arch=ARM64 ;; \
|
|
*) echo "unsupported arch: $arch"; exit 1 ;; \
|
|
esac; \
|
|
curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${gl_arch}.tar.gz" \
|
|
| tar -xz -C /usr/local/bin gitleaks; \
|
|
curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-${tv_arch}.tar.gz" \
|
|
| tar -xz -C /usr/local/bin trivy; \
|
|
gitleaks version; trivy --version | head -1
|
|
|
|
# semgrep in its own venv so its pinned dependency tree cannot collide with
|
|
# anything else installed here.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
python3 python3-pip python3-venv \
|
|
&& python3 -m venv /opt/semgrep \
|
|
&& /opt/semgrep/bin/pip install --no-cache-dir semgrep \
|
|
&& ln -s /opt/semgrep/bin/semgrep /usr/local/bin/semgrep \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& semgrep --version
|
|
|
|
# Rust last: the largest layer and the one most likely to be bumped, so it sits
|
|
# where a rebuild costs the least cache. `templates/teams/rust_sdlc.toml` tells
|
|
# the coder to run `cargo test`, and the evaluator runs it again to check.
|
|
ENV RUSTUP_HOME=/usr/local/rustup \
|
|
CARGO_HOME=/usr/local/cargo \
|
|
PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
gcc libc6-dev pkg-config libssl-dev make cmake build-essential python3-dev \
|
|
&& curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable \
|
|
&& cargo install cargo-audit --locked --no-default-features \
|
|
&& rm -rf /var/lib/apt/lists/* "$CARGO_HOME/registry" "$CARGO_HOME/git" \
|
|
&& chmod -R a+rX "$RUSTUP_HOME" "$CARGO_HOME" \
|
|
&& rustc --version && cargo audit --version
|
|
|
|
# PATH is spelled out in full above rather than as `/usr/local/cargo/bin:$PATH`
|
|
# on purpose. `scripts/fc-build-rootfs.sh` reads the image's resolved
|
|
# `Config.Env` into the guest's /etc/profile.d, and that is the only way the
|
|
# VM learns its PATH — `docker export` carries no image metadata at all. An
|
|
# interpolated value resolves at build time, so this is equivalent; being
|
|
# explicit means the guest's PATH is readable here rather than inferred.
|
|
|
|
# Where a mission's checkout is injected, on both the container and the microVM
|
|
# path. Created here so the guest agent's first write is not also a mkdir.
|
|
RUN mkdir -p /mission/repo
|
|
WORKDIR /mission/repo
|
|
|
|
# Idle keep-alive. On the container path the orchestrator execs work in; on the
|
|
# microVM path pid 1 is the guest agent instead and this is never reached.
|
|
CMD ["sleep", "infinity"]
|