The only rootfs on this track came from clawmates/agent-terminal:dev. Mounted,
it held git and nothing else: no claude, no node, no cargo. A VM booted from it
looks perfect and cannot run a mission, so B4.5 could have been written and
never verified.
images/agent-toolchain — the shared mission toolchain (node 22, git, rust +
cargo-audit, gitleaks/trivy/semgrep, tea/gitea-mcp), lifted from the proven
deploy/clawmates-runtime image minus the zeroclaw daemon: a microVM mission runs
the direct-session model, so there is no daemon to host. A base image rather
than three self-contained Dockerfiles because this layer is ~3 GB and the real
risk is scanner and toolchain versions drifting between per-CLI images — the
evaluator runs the project's own suite to check a claim, so `cargo` present in
one image and absent in another makes the same mission pass or fail by backend
with nothing saying why.
images/agent-claude — plan A6, first of three: the pinned CLI and its env
contract only, so bumping Claude Code does not rebuild the toolchain and cannot
disturb agent-kimi / agent-glm. HOME=/root with an empty .claude for B4.4 to
inject into; no ANTHROPIC_API_KEY, since it silently overrides the subscription
OAuth we already pay for.
Both the builder and the node selftest now ASK the guest for the CLI the image
is named for, instead of trusting the name. `required_cli` maps claude/kimi/glm
to a probe; an unrecognised backend reports unchecked and prints SKIP rather
than passing quietly.
Verified on tank:
- rootfs-claude.ext4 boots; git, node, cargo, a real git commit all work
- `claude --version` → 2.1.220 over vsock, in both the builder and
`--vm-selftest` (11/11, create 1498 ms)
- negative control: the same builder run against agent-terminal with
FC_CLI forced reports `cli rc=127 claude: not found` and exits 1, so the
green result above is a measurement and not a default
- `claude -p hello` fails with "Not logged in · Please run /login" — the CLI
runs headless in the VM, and B4.4 only has to supply the credential
- no leaked firecracker processes or vm dirs afterwards
437 tests pass, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
120 lines
6.2 KiB
Docker
120 lines
6.2 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.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
ca-certificates curl gnupg git jq less procps \
|
|
&& 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 \
|
|
&& 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"]
|