# 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_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"]