| 1 | # anvil-runner — the shared execution image for BOTH CI jobs and agent |
| 2 | # sessions. |
| 3 | # |
| 4 | # CI pipelines that omit `image:` land here (config `ci.default_image`), and |
| 5 | # agent sessions always do (`agent.image`). Keeping them the same image means a |
| 6 | # session can reproduce a build by hand, and there is one thing to keep current. |
| 7 | # |
| 8 | # Parameterized by base so the same recipe produces a small general runner and |
| 9 | # a toolchain-carrying one: |
| 10 | # |
| 11 | # anvil-runner:latest BASE=debian:bookworm-slim (the default) |
| 12 | # anvil-runner:rust BASE=rust:1.95-bookworm (builds anvil itself) |
| 13 | # |
| 14 | # Build both with deploy/runner/build.sh. There is NO registry behind this |
| 15 | # image — it lives only in the host's local image store, which is why anvil |
| 16 | # treats a failed pull of the default image as non-fatal. |
| 17 | ARG BASE=debian:bookworm-slim |
| 18 | FROM ${BASE} |
| 19 | |
| 20 | # Which Claude Code release channel to track. `stable` is roughly a week behind |
| 21 | # and skips releases with known major regressions; `latest` ships immediately. |
| 22 | ARG CLAUDE_CHANNEL=stable |
| 23 | |
| 24 | ENV DEBIAN_FRONTEND=noninteractive |
| 25 | |
| 26 | # Fingerprint of the Claude Code release signing key, checked below so a |
| 27 | # substituted key fails the build rather than silently installing. Published at |
| 28 | # https://code.claude.com/docs/en/setup. Deliberately inlined rather than an |
| 29 | # ARG: a build arg could be overridden on the command line, which would defeat |
| 30 | # the pin it exists to enforce. |
| 31 | |
| 32 | # tmux — session persistence; the whole point of the agent design |
| 33 | # git — the container clones/pushes for itself (anvil's own code never |
| 34 | # shells out to git, but what a job runs is its own tooling) |
| 35 | # fish — the interactive shell you actually get when you attach |
| 36 | # ripgrep — Claude Code's search backend |
| 37 | # curl/gnupg/ca-certificates — fetching and verifying the apt repo key |
| 38 | RUN apt-get update \ |
| 39 | && apt-get install -y --no-install-recommends \ |
| 40 | ca-certificates \ |
| 41 | curl \ |
| 42 | fish \ |
| 43 | git \ |
| 44 | gnupg \ |
| 45 | less \ |
| 46 | ripgrep \ |
| 47 | tmux \ |
| 48 | && install -d -m 0755 /etc/apt/keyrings \ |
| 49 | && curl -fsSL https://downloads.claude.ai/keys/claude-code.asc \ |
| 50 | -o /etc/apt/keyrings/claude-code.asc \ |
| 51 | && gpg --show-keys --with-colons /etc/apt/keyrings/claude-code.asc \ |
| 52 | | awk -F: '/^fpr:/ {print $10}' \ |
| 53 | | grep -qx 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE \ |
| 54 | && echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc]" \ |
| 55 | "https://downloads.claude.ai/claude-code/apt/${CLAUDE_CHANNEL}" \ |
| 56 | "${CLAUDE_CHANNEL} main" > /etc/apt/sources.list.d/claude-code.list \ |
| 57 | && apt-get update \ |
| 58 | && apt-get install -y --no-install-recommends claude-code \ |
| 59 | && rm -rf /var/lib/apt/lists/* |
| 60 | |
| 61 | # apt installs never auto-update, but Claude Code still checks on startup and |
| 62 | # the check is pure noise in a container whose version is pinned by the image. |
| 63 | ENV DISABLE_AUTOUPDATER=1 |
| 64 | |
| 65 | # An unprivileged user for agent sessions to run as. Deliberately NOT set as |
| 66 | # the image's USER: CI pipelines inherit this image's default user, and plenty |
| 67 | # of them expect root for apt-get. anvil passes `user: agent` explicitly when |
| 68 | # it creates a *session* container, so CI keeps the behaviour it has today. |
| 69 | RUN useradd --create-home --shell /usr/bin/fish --uid 1000 agent \ |
| 70 | && mkdir -p /workspace \ |
| 71 | && chown agent:agent /workspace |
| 72 | |
| 73 | COPY tmux.conf /etc/anvil/tmux.conf |
| 74 | COPY session-entrypoint.sh /usr/local/bin/anvil-session |
| 75 | RUN chmod 0755 /usr/local/bin/anvil-session |
| 76 | |
| 77 | WORKDIR /workspace |