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
|
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
|
USER root
|
||||||
RUN apt-get update \
|
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/*
|
&& 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
|
USER agent
|
||||||
|
|
||||||
COPY --chown=agent:agent claude/ /home/agent/.claude/
|
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 |
|
| `claude/hooks`, `claude/skills` | `~/.claude`, verbatim |
|
||||||
| `plugins.json` | generated from `~/.claude/plugins/known_marketplaces.json` and `settings.json` |
|
| `plugins.json` | generated from `~/.claude/plugins/known_marketplaces.json` and `settings.json` |
|
||||||
| mise | `https://mise.run`, with shims on `PATH` via `.bashrc` |
|
| 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
|
Credentials, transcripts, `history.jsonl`, `projects/`, `file-history/` and
|
||||||
`session-env/` are never copied. `~/.claude/plugins/` is not copied either — the build
|
`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
|
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.
|
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
|
## Pushing
|
||||||
|
|
||||||
`docker push` **fails** against this registry: `git.mroberts.dev` is behind Cloudflare,
|
`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