Add tmux, a pinned Neovim and the workspace launcher
build / Build and push image (push) Successful in 4m51s
build / Build and push image (push) Successful in 4m51s
Removes the per-sandbox setup cost AI_SBX_TOOLS pays on every setup, and makes the sandbox a terminal environment worth working in. Neovim comes from the upstream tarball rather than apt: Ubuntu's build is far behind what a LazyVim config needs. Upstream publishes no checksums, so NEOVIM_SHA256 is taken from the release asset and verified at build time. The version is pinned and labelled because a config that works on the host and breaks in the sandbox on a version skew is expensive to diagnose. The launcher is fetched from ai-sandbox v1.11.0's tasks/ai/workspace rather than vendored here. The task installs its own copy into stock-image sandboxes and skips that when the image supplies one, so two copies could drift and make behaviour depend on which image you are on. A bash -n guard rejects a forge error page served with a 200.
This commit is contained in:
+32
-1
@@ -1,10 +1,41 @@
|
||||
FROM docker/sandbox-templates:claude-code-docker
|
||||
|
||||
# Pinned deliberately: a Neovim config that works on the host and breaks in the
|
||||
# sandbox because of a version skew is expensive to diagnose. Upstream publishes
|
||||
# no checksums, so this digest was taken from the release asset itself.
|
||||
ARG NEOVIM_VERSION=v0.12.4
|
||||
ARG NEOVIM_SHA256=012bf3fcac5ade43914df3f174668bf64d05e049a4f032a388c027b1ebd78628
|
||||
|
||||
# The launcher is built from ai-sandbox's tasks/ai/workspace, never written
|
||||
# independently — the task skips installing its own copy when the image supplies
|
||||
# one, so a drifting copy would make behaviour depend on which image you are on.
|
||||
ARG AI_SANDBOX_REF=v1.11.0
|
||||
|
||||
LABEL dev.mroberts.claude-sbx.neovim="${NEOVIM_VERSION}" \
|
||||
dev.mroberts.claude-sbx.ai-sandbox-ref="${AI_SANDBOX_REF}"
|
||||
|
||||
USER root
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends jq \
|
||||
&& apt-get install -y --no-install-recommends jq tmux \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ponytail: amd64 only — the sandbox microVM is x86_64. Add a uname case and a
|
||||
# second digest if this ever needs to build on arm64.
|
||||
RUN curl -fsSL -o /tmp/nvim.tar.gz \
|
||||
"https://github.com/neovim/neovim/releases/download/${NEOVIM_VERSION}/nvim-linux-x86_64.tar.gz" \
|
||||
&& printf '%s /tmp/nvim.tar.gz\n' "${NEOVIM_SHA256}" | sha256sum -c - \
|
||||
&& tar -xzf /tmp/nvim.tar.gz -C /usr/local --strip-components=1 \
|
||||
&& rm -f /tmp/nvim.tar.gz \
|
||||
&& nvim --version | head -1
|
||||
|
||||
ADD --chmod=755 \
|
||||
"https://git.mroberts.dev/mroberts/ai-sandbox/raw/tag/${AI_SANDBOX_REF}/tasks/ai/workspace" \
|
||||
/usr/local/bin/ai-sbx-workspace
|
||||
|
||||
# A forge that answers a missing path with an HTML error page would otherwise
|
||||
# produce an executable that fails only when someone tries to use it.
|
||||
RUN bash -n /usr/local/bin/ai-sbx-workspace
|
||||
|
||||
USER agent
|
||||
|
||||
COPY --chown=agent:agent claude/ /home/agent/.claude/
|
||||
|
||||
@@ -43,6 +43,19 @@ created from it sees any of that — and it will not until #366 is fixed.
|
||||
| `claude/hooks`, `claude/skills` | `~/.claude`, verbatim |
|
||||
| `plugins.json` | generated from `~/.claude/plugins/known_marketplaces.json` and `settings.json` |
|
||||
| mise | `https://mise.run`, with shims on `PATH` via `.bashrc` |
|
||||
| `tmux` | Ubuntu `apt`, 3.6a |
|
||||
| `nvim` | upstream release tarball into `/usr/local`, pinned and checksummed |
|
||||
| `/usr/local/bin/ai-sbx-workspace` | fetched from `ai-sandbox` at the tag in `AI_SANDBOX_REF` |
|
||||
|
||||
The pinned versions are recorded in image labels
|
||||
(`dev.mroberts.claude-sbx.neovim`, `dev.mroberts.claude-sbx.ai-sandbox-ref`), so a
|
||||
sandbox that disagrees with the host config can be diagnosed from the image alone.
|
||||
|
||||
Neovim comes from upstream rather than `apt` because Ubuntu's build is far behind what
|
||||
a LazyVim config needs. Upstream publishes no checksums, so `NEOVIM_SHA256` was taken
|
||||
from the release asset itself and is verified at build time.
|
||||
|
||||
With tmux and Neovim in the image, `AI_SBX_TOOLS` can drop back to `bun`.
|
||||
|
||||
Credentials, transcripts, `history.jsonl`, `projects/`, `file-history/` and
|
||||
`session-env/` are never copied. `~/.claude/plugins/` is not copied either — the build
|
||||
@@ -60,6 +73,20 @@ docker build -t git.mroberts.dev/mroberts/claude-sbx:v1 .
|
||||
Requires no access to `~/.claude`. The build installs 10 marketplaces and 17 plugins
|
||||
and fails if the resulting enabled-plugin count does not match the manifest.
|
||||
|
||||
The launcher is fetched from the `ai-sandbox` tag named by `AI_SANDBOX_REF`, rather
|
||||
than vendored here. The task installs its own copy into sandboxes running a stock
|
||||
image and skips that when the image supplies one, so two copies that could drift would
|
||||
make behaviour depend on which image you happen to be on. Build against a different
|
||||
release with:
|
||||
|
||||
```bash
|
||||
docker build --build-arg AI_SANDBOX_REF=v1.12.0 -t git.mroberts.dev/mroberts/claude-sbx:v2 .
|
||||
```
|
||||
|
||||
The default is `v1.11.0`, the `ai-sandbox` release that introduced the launcher. A tag
|
||||
without `tasks/ai/workspace` fails the build at the fetch rather than shipping a
|
||||
broken launcher.
|
||||
|
||||
## Pushing
|
||||
|
||||
`docker push` **fails** against this registry: `git.mroberts.dev` is behind Cloudflare,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# Spec: base sandbox image — `claude-sbx`
|
||||
|
||||
Implementation spec for the image. The consuming task and its configuration surfaces
|
||||
are specified in `mroberts/ai-sandbox` → `docs/tmux-workspace-spec.md`. Registry,
|
||||
workflow, and plugin-manifest work are in [`HANDOFF.md`](HANDOFF.md).
|
||||
|
||||
## Purpose
|
||||
|
||||
Remove the per-sandbox setup cost that `AI_SBX_TOOLS` currently pays on every `setup`,
|
||||
and provide a terminal environment good enough to work in rather than only to watch an
|
||||
agent in.
|
||||
|
||||
The image carries **tools and the launcher**. It carries **no dotfiles and no
|
||||
credentials** — those arrive at runtime from the host, because they are personal,
|
||||
change often, and in the dotfiles case contain secrets.
|
||||
|
||||
## What the base image already provides
|
||||
|
||||
Verified against `docker/sandbox-templates:claude-code-docker`:
|
||||
|
||||
| Present | Absent |
|
||||
| --- | --- |
|
||||
| `git`, `node`, `npm`, `docker` (29.6.1) | `tmux`, `nvim`, `vim` |
|
||||
|
||||
Also guaranteed by the Sandboxes base image contract, and must be preserved:
|
||||
|
||||
- non-root `agent` at UID 1000 with passwordless sudo
|
||||
- `/home/agent/` owned by `agent`
|
||||
- `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` preserved across sudo
|
||||
|
||||
## Contents
|
||||
|
||||
### 1. tmux
|
||||
|
||||
`apt-get install -y --no-install-recommends tmux` as `root`. Ubuntu's build is current
|
||||
enough; nothing here depends on a recent tmux.
|
||||
|
||||
### 2. Neovim
|
||||
|
||||
**Not from `apt`.** Ubuntu ships a version far behind what a LazyVim-based config
|
||||
needs. Install the upstream release tarball to `/usr/local`, or keep neovim on mise.
|
||||
|
||||
Pin the version explicitly and record it in the image labels — a config that works on
|
||||
the host and breaks in the sandbox because of a version skew is a bad afternoon.
|
||||
|
||||
### 3. The launcher
|
||||
|
||||
`/usr/local/bin/ai-sbx-workspace`, mode 755.
|
||||
|
||||
**Built from `ai-sandbox`'s `tasks/ai/workspace`, not written independently.** The task
|
||||
installs its own copy into sandboxes running a stock image; if the image's copy drifts,
|
||||
behaviour depends on which image you happen to be on. Vendor it at build time (fetch
|
||||
from the tagged `ai-sandbox` release the image is built against) and record that tag in
|
||||
a label.
|
||||
|
||||
Behaviour is specified in the task spec. In summary: attach-or-create session `ai-sbx`,
|
||||
three windows (`agent`, `edit`, `shell`), all in the workspace root, agent window
|
||||
running `claude --dangerously-skip-permissions`.
|
||||
|
||||
### 4. Optional: pre-warmed plugins
|
||||
|
||||
The full Neovim config clones 93 plugins and installs up to 55 mason packages on first
|
||||
launch. Every host that needs is already permitted by the `Balanced` network policy, so
|
||||
this is slow rather than blocked.
|
||||
|
||||
Pre-warming `~/.local/share/nvim` into the image removes that wait at the cost of a
|
||||
much larger image and staleness against `lazy-lock.json`. **Defer this.** Measure the
|
||||
first-launch cost with the plain image before deciding it is a problem worth an extra
|
||||
gigabyte.
|
||||
|
||||
### 5. Deliberately excluded
|
||||
|
||||
| Excluded | Why |
|
||||
| --- | --- |
|
||||
| Dotfiles | Personal, change often, and the image is pushed to a registry |
|
||||
| Credentials | Provisioned at runtime as proxy-substituted placeholders |
|
||||
| `~/.claude` config and plugins | The task copies these per sandbox from the host |
|
||||
| The chezmoi age key | Never leaves the host |
|
||||
|
||||
## Interaction with the task
|
||||
|
||||
The image and the task must agree on exactly two things:
|
||||
|
||||
| Contract | Owner |
|
||||
| --- | --- |
|
||||
| `/usr/local/bin/ai-sbx-workspace` exists and behaves as specified | image |
|
||||
| Skip installing the launcher when the image supplies it | task |
|
||||
|
||||
Everything else — Claude config, plugins, mise, secrets, network policy — is already
|
||||
handled at runtime and needs nothing from the image.
|
||||
|
||||
Consumed by setting, in `~/.config/mise/config.toml`:
|
||||
|
||||
```toml
|
||||
AI_SBX_TEMPLATE = "git.mroberts.dev/mroberts/claude-sbx:v1"
|
||||
```
|
||||
|
||||
Once the image carries tmux and neovim, `AI_SBX_TOOLS` can drop back to `bun`.
|
||||
|
||||
## Build and publish
|
||||
|
||||
Per `HANDOFF.md`: Gitea Actions, pushing to `git.mroberts.dev/mroberts/claude-sbx`.
|
||||
|
||||
- Tag immutably. `AI_SBX_TEMPLATE` pins one ref, and a moving tag means an unreviewed
|
||||
image runs on the developer's workstation with their credentials.
|
||||
- Confirm an Actions runner is registered before writing the workflow —
|
||||
`has_actions: true` only means the feature is enabled. Fall back to
|
||||
`docker build --push` from a workstation if there is none.
|
||||
- The sandbox pulls from the registry directly and does not share the host image
|
||||
store, so a local build is invisible to it. Use `sbx secret set --registry
|
||||
git.mroberts.dev` for pull credentials.
|
||||
|
||||
## Acceptance
|
||||
|
||||
1. `docker build` succeeds from a clean checkout with no access to `~/.claude` or
|
||||
`~/.local/share/chezmoi`.
|
||||
2. In a sandbox created with `AI_SBX_TEMPLATE` set to the pushed image:
|
||||
- `tmux -V` and `nvim --version` both report the pinned versions
|
||||
- `ai-sbx-workspace` exists and is executable
|
||||
- `id -u` is 1000 and `sudo true` succeeds
|
||||
- `printenv HTTPS_PROXY` is non-empty under `sudo -E`
|
||||
3. `mise run ai:sbx -- run` with `AI_SBX_LAUNCH=tmux` opens three windows without the
|
||||
task installing anything.
|
||||
4. No credential, transcript, or dotfile is present in any layer — checked by
|
||||
extracting layers, not by inspecting the build context.
|
||||
5. `AI_SBX_TOOLS="bun"` is sufficient; nothing regresses from dropping tmux and neovim
|
||||
out of it.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Neovim delivery.** Upstream tarball, mise, or a PPA. Tarball is the most
|
||||
predictable and the easiest to pin.
|
||||
- **Image size.** Measure before and after. The `-docker` base is already substantial;
|
||||
tmux and neovim are small, pre-warmed plugins are not.
|
||||
- **Rebuild cadence.** The image lags the host config by definition. Decide whether it
|
||||
tracks a tagged `claude-sbx` release or is rebuilt on dotfile changes — the answer
|
||||
determines whether pre-warming is even coherent.
|
||||
Reference in New Issue
Block a user