anvilsign in

collin/anvil

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 so the same recipe produces a small general runner and a
9# toolchain-carrying one:
10#
11# anvil-runner:latest RUST_VERSION= (the default)
12# anvil-runner:rust RUST_VERSION=1.98.0 (builds anvil itself)
13#
14# Both sit on ubuntu:26.04. The official Rust image is Debian-based and has no
15# Ubuntu variant, so rather than let one tag drift onto a different distro the
16# toolchain is installed here with rustup; `RUST_VERSION` empty means the slim
17# tag and skips that layer entirely.
18#
19# Build both with deploy/runner/build.sh. There is NO registry behind this
20# image — it lives only in the host's local image store, which is why anvil
21# treats a failed pull of the default image as non-fatal.
22ARG BASE=ubuntu:26.04
23FROM ${BASE}
24
25# Which Claude Code release channel to track. `stable` is roughly a week behind
26# and skips releases with known major regressions; `latest` ships immediately.
27ARG CLAUDE_CHANNEL=stable
28
29ENV DEBIAN_FRONTEND=noninteractive
30
31# A TUI (Claude Code's own, or anything a session runs) that thinks it's stuck
32# on a 7-bit terminal falls back to ASCII line-drawing instead of real box-
33# drawing/bullet glyphs. glibc's built-in C.UTF-8 needs no locale-gen step and
34# is present on both Ubuntu and Debian bases.
35ENV LANG=C.UTF-8
36ENV LC_ALL=C.UTF-8
37
38# Fingerprint of the Claude Code release signing key, checked below so a
39# substituted key fails the build rather than silently installing. Published at
40# https://code.claude.com/docs/en/setup. Deliberately inlined rather than an
41# ARG: a build arg could be overridden on the command line, which would defeat
42# the pin it exists to enforce.
43
44# tmux — session persistence; the whole point of the agent design. Needs
45# 3.4+ for OSC 8 hyperlink passthrough (a Claude Code TUI's login
46# URL, most visibly) — see tmux.conf for the other half (telling
47# tmux the attached client accepts it). Ubuntu 26.04 ships 3.6a;
48# this is the reason BASE moved off Debian bookworm (stuck on
49# 3.3a, pre-dating that support entirely).
50# git — the container clones/pushes for itself (anvil's own code never
51# shells out to git, but what a job runs is its own tooling)
52# fish — the interactive shell you actually get when you attach
53# neovim — a sensible $EDITOR for anything a session or an attached human
54# shells out to (git commit messages, etc.)
55# ripgrep — Claude Code's search backend
56# curl/gnupg/ca-certificates — fetching and verifying the apt repo key
57RUN apt-get update \
58 && apt-get install -y --no-install-recommends \
59 ca-certificates \
60 curl \
61 fish \
62 git \
63 gnupg \
64 less \
65 neovim \
66 ripgrep \
67 tmux \
68 && install -d -m 0755 /etc/apt/keyrings \
69 && curl -fsSL https://downloads.claude.ai/keys/claude-code.asc \
70 -o /etc/apt/keyrings/claude-code.asc \
71 && gpg --show-keys --with-colons /etc/apt/keyrings/claude-code.asc \
72 | awk -F: '/^fpr:/ {print $10}' \
73 | grep -qx 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE \
74 && echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc]" \
75 "https://downloads.claude.ai/claude-code/apt/${CLAUDE_CHANNEL}" \
76 "${CLAUDE_CHANNEL} main" > /etc/apt/sources.list.d/claude-code.list \
77 && apt-get update \
78 && apt-get install -y --no-install-recommends claude-code \
79 && rm -rf /var/lib/apt/lists/*
80
81# The Rust toolchain, for the `rust` tag only. Installed rather than inherited
82# from `rust:<ver>-bookworm` so both tags share one base — Debian bookworm's
83# tmux is the reason the slim tag left it (above), and one distro means a
84# session can reproduce a CI build without accounting for the difference.
85# Empty `RUST_VERSION` is the slim tag: no rustup, no compiler, no extra layer.
86# The three ENVs are set unconditionally (Dockerfile has no conditional ENV);
87# on the slim tag they just name directories that do not exist.
88ARG RUST_VERSION=
89ENV RUSTUP_HOME=/usr/local/rustup
90ENV CARGO_HOME=/usr/local/cargo
91ENV PATH=/usr/local/cargo/bin:$PATH
92RUN if [ -n "${RUST_VERSION}" ]; then \
93 apt-get update \
94 && apt-get install -y --no-install-recommends \
95 build-essential \
96 pkg-config \
97 && rm -rf /var/lib/apt/lists/* \
98 && curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \
99 | sh -s -- -y --no-modify-path --profile minimal \
100 --default-toolchain "${RUST_VERSION}" \
101 && chmod -R a+w "${RUSTUP_HOME}" "${CARGO_HOME}" \
102 && rustc --version && cargo --version; \
103 fi
104
105# apt installs never auto-update, but Claude Code still checks on startup and
106# the check is pure noise in a container whose version is pinned by the image.
107ENV DISABLE_AUTOUPDATER=1
108
109# What `git commit` (no -m) and anything else honouring $EDITOR drops you
110# into. Ubuntu's neovim package doesn't register update-alternatives entries
111# for vi/vim/editor, so this is the only thing that makes it the default.
112ENV EDITOR=nvim
113ENV VISUAL=nvim
114
115# An unprivileged user for agent sessions to run as. Deliberately NOT set as
116# the image's USER: CI pipelines inherit this image's default user, and plenty
117# of them expect root for apt-get. anvil passes `user: ubuntu` explicitly when
118# it creates a *session* container, so CI keeps the behaviour it has today.
119#
120# Reusing the base image's own `ubuntu` user (uid 1000, matching
121# [`container::RUN_AS_UID`]) rather than making a purpose-built account: it's
122# already there, so this is just pointing its shell at fish and giving it a
123# workspace under its own home ([`container::WORKDIR`]) — under `$HOME` rather
124# than a bare `/workspace` so tools that assume a project lives there behave.
125RUN usermod --shell /usr/bin/fish ubuntu \
126 && mkdir -p /home/ubuntu/workspace \
127 && chown ubuntu:ubuntu /home/ubuntu/workspace
128
129# Baseline Claude Code config for a session: auto theme (so it reads fine
130# however the browser terminal is themed) and bypass-permissions, matching the
131# `--dangerously-skip-permissions` flag the session launches with (see
132# anvil-agent/src/supervisor.rs) — belt and suspenders for anything that reads
133# the setting rather than the flag. `agent.credentials_dir`, when configured,
134# uploads over `~/.claude` and can add to or override this file.
135COPY claude-settings.json /home/ubuntu/.claude/settings.json
136RUN chown -R ubuntu:ubuntu /home/ubuntu/.claude
137
138# settings.json's `theme` only sets the *value*; it does not mark the
139# first-run wizard as done, and that state lives in a separate file. Without
140# this, every session replays the theme picker and the per-project trust
141# dialog regardless of what settings.json says. Keyed on [`container::WORKDIR`]
142# — fixed for every session, so this is safe to bake in rather than derive at
143# container-creation time. Login still prompts (there
144# is nothing to skip: a fresh session has no credentials until
145# `agent.credentials_dir` is configured), and that dialog's OSC 8 link is the
146# one tmux.conf's `terminal-features` line exists for.
147COPY claude-onboarding.json /home/ubuntu/.claude.json
148RUN chown ubuntu:ubuntu /home/ubuntu/.claude.json
149
150COPY tmux.conf /etc/anvil/tmux.conf
151COPY session-entrypoint.sh /usr/local/bin/anvil-session
152RUN chmod 0755 /usr/local/bin/anvil-session
153
154WORKDIR /home/ubuntu/workspace