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