Add tmux, a pinned Neovim and the workspace launcher
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:
2026-08-03 15:38:31 -05:00
parent 9be4122580
commit 1617bb15df
3 changed files with 196 additions and 1 deletions
+137
View File
@@ -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.