049: A project's image is a script on the standard image, and images are rebuilt when stale
Status: accepted, 2 October 2026. Extends 029: the configuration holds images; 033: a new box takes its project’s image; 032: spares can be of an image; and 042: Docker images are rebuilt through the same relay. Extended by 050: standard Linux images carry Tailscale, stopped and with no state. Amended before it was built, 3 October 2026: a person never writes the script. It lives in the project’s repository, and an agent writes it from a draft that bb-box makes from the project’s files.
Context
Every box of a kind starts from the same image: a Proxmox LXC template, a Docker image, a Tart VM or a QEMU disk. The operator asked for two things.
- Their own setup on top. A project should be able to add what its threads need, such as Rust and Tailscale for a project that shares a keyboard and mouse over a tailnet, once for every backend, and have its threads’ boxes start with it. They added later that no person should have to write a script for it by hand.
- Freshness. Images are rebuilt only when a person runs a setup script. Each should say when it was built and what is in it, the doctor should say when one is old, and a nightly job should rebuild them.
What exists:
- Builds. Proxmox LXC templates come from
template/build.sh, run as root on the Proxmox host. It runs the guest scripts in a container (customize-base.sh, thencustomize-desktop.shfor a desktop), the kind’s check as the box user, thenfinalize.sh. Docker images come fromsetup/docker/build.sh, which runs the same guest scripts, and which bb-box runs itself for a Docker host that is a BB machine (042). Tart’s images come fromprepare-image.sh,prepare-linux-image.sh(the same guest scripts) andprepare-windows-image.sh, run on the Mac. - Records.
finalize.shwrites/usr/share/bb-box/template-release(kind, date, apt snapshot, and each tool’s version and SHA-256),template-pins.jsonandtemplate-packages.tsv. LXC templates carry a build ID tag, which warm guests copy. Docker images carry labels with the layout version and the digest of BB’s host app. Tart’s builds are told apart by the disk’s inode and time; Windows images have abuild.json. - What is checked. Warm guests are retired when their template’s build ID changes, or Tart’s image, Tart or macOS does. The Docker doctor says when an image is missing, from another layout or has another build of BB’s app. A GPU desktop compares its NVIDIA userspace with the host’s driver at each start.
- Pins.
pins.jsonanddesktop-pins.tsvpin each tool and the apt snapshot by hand. A rebuild with the same pins changes only BB’s app and the apt lists.
A layer like the example project’s, in a linux box on Proxmox LXC:
| Step | Time |
|---|---|
| Tailscale from its apt repository | 3.0 s |
| Rust’s minimal toolchain with rustup | 17.7 s |
| clippy and rustfmt | 1.0 s |
| All, as a script at box start | 21.8 s, and 0.6 GB on disk |
A box from a spare or warm guest is ready in under 2 s, so the script would be most of every box’s wait. A script that also builds a project’s dependencies takes minutes.
Decision
The contract
Any image bb-box starts a box from, its own or a project’s, must keep this contract. The kind’s check, which every build already runs, checks it.
- The box user.
box, with sudo without a password. On backends reached over SSH (Proxmox LXC, Tart, QEMU), bb-box’s public key in itsauthorized_keysandsshdat boot. Docker boxes are reached throughdocker exec(031) and need neither. - bb-box’s boot units. Clearing BB’s daemon state at boot (017) and reading the names the backend gives the guest (018). The image itself names no install or box.
- BB’s host app with its digest file, so a box enrols in about 2 s rather than 5. Not on Windows, where BB’s app does not run.
- Docker, started on first use (041), on Linux.
- For desktop kinds, the kind’s desktop and its units (Xorg or Xtigervnc with XFCE, or Sway with wayvnc (048)), the display script that resizes (021), Chrome, and Cua Driver with its unit (015).
- Nothing of one machine. No SSH host keys, machine ID, logs, caches, Docker state, BB daemon state or other per-machine keys, so each box makes its own.
- Its record, below.
The contract has a version, in the record. The doctor fails an image whose version is older than bb-box needs, as it does now for Docker’s layout version, which this replaces.
A project’s image
An image is a script run as root on a kind’s standard image, built for each backend that offers the kind.
- In the configuration. Each image has a name such as
rust-tools, the kinds it builds on, a script for each OS family (Linux now; Windows in PowerShell and macOS later), an optional check run as the box user, how it is baked (below), the projects it belongs to, and where the script came from in the project’s repository. - Set by an agent, the CLI or the page.
box_image_set, orbb box image set <name> --kinds linux,linux-desktop-cpu --script-file .bb/box-setup.sh [--check-file .bb/box-check.sh] --project <id>, keeps a copy of the files, as BB’s script automations do. The settings page lists each image with its builds per backend, their dates, sizes and states, and the last build’s output. - One script for every Linux backend. It runs in a booted box with the
network, on Proxmox LXC, Docker and Tart, x86_64 and arm64. It can read
BB_BOX_IMAGE,BB_BOX_KIND,BB_BOX_BACKENDandBB_BOX_ARCHwhere it must differ. It removes any per-machine state it makes; finalizing removes only what bb-box knows of. - No secrets. The build box is not a BB machine, so it has none of the project’s variables, and what an image holds every box from it gets. Joining a tailnet is done per box, at start (050).
The example project’s .bb/box-setup.sh:
#!/bin/bashset -euo pipefailcurl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg \ -o /usr/share/keyrings/tailscale-archive-keyring.gpgcurl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list \ -o /etc/apt/sources.list.d/tailscale.listapt-get update && apt-get install -y tailscale# No node of its own: a box joins at start (050).systemctl disable --now tailscaled && rm -rf /var/lib/tailscalesudo -iu box sh -c 'curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal -c clippy -c rustfmt'Writing the script
A person never writes the script. It is still what is stored and built, but an agent writes it on request, from a draft bb-box makes from what the project already has.
- In the repository. The script is
.bb/box-setup.shin the project’s repository by default, beside an optional.bb/box-check.sh, so it is reviewed and changed with the code that needs it. Registering it copies it into the configuration with its path, and builds use that copy. A change in the repository counts once it is registered again. - On request. A person asks in a thread, such as “make a box image for this project”. When a project has no image, New box offers “Ask an agent to make one”, which starts a thread with that request.
- The flow, in the bb-box skill:
box_image_draftreads the project’s files from the thread’s workspace and returns a draft, each part commented with the file it came from, and a list of what it could not translate.- The agent fills the gaps and writes
.bb/box-setup.sh, and a check of what the project needs, such ascargo --version, in.bb/box-check.sh. box_image_tryruns the script as a build does: in a new box of the base kind from the current standard image, as root with theBB_BOX_*variables, then finalizing, the kind’s check and the image’s check. It returns the time, the last 200 lines and each check’s result, then releases the box, or keeps it withkeep: trueso the agent can look inside. The agent repeats until the checks pass, from a clean base each time, as the build will be.- The agent commits the files and registers them with
box_image_set. Registering changes every new box in the project’s threads, so the agent asks the person first unless they asked for that. bb-box then runs the script at start or bakes it, as below.
- No repository at build time. The build box has no checkout, so the
script cannot read the repository. What needs it, such as
npm ciorcargo fetch, stays in the thread, and the draft says so.
What bb-box drafts from:
| The project has | The draft | Left to the agent |
|---|---|---|
devcontainer.json features |
nothing for what standard images have, such as Docker in Docker, the GitHub CLI and common utilities; mise for languages; other features through bb-box-feature, which fetches the feature and runs its install.sh with its options, as the Dev Container spec says |
features for another OS family |
devcontainer.json containerEnv, onCreateCommand, postCreateCommand |
the variables in /etc/profile.d; the commands, commented out |
which commands need no workspace |
| A Dockerfile from Ubuntu or Debian | RUN lines in order, ENV and ARG as variables, and a USER other than root as the box user |
COPY and ADD, other bases and multi-stage builds |
.tool-versions, mise.toml, .nvmrc, .python-version |
mise for the box user, with each tool at its version | tools mise does not have |
rust-toolchain.toml |
rustup with its channel, components and targets | |
flake.nix or shell.nix |
Nix, and the dev shell built once from copies of the flake and its lock in the script | |
| none of these | a script with a comment for each part to fill in | all of it |
Considered and rejected: saving a box as the image. A “Save this box as an image” button would capture a box someone set up by hand. It is not the stored form:
- It cannot be built again on a fresh standard image, so nightly rebuilds and contract changes would leave it behind.
- It keeps whatever happened in the box: caches, shell history, the thread’s files, BB’s enrolment, secrets typed in.
- A capture serves one backend. A script serves Proxmox LXC, Docker and Tart.
- No one can review or change it in a pull request.
At most it is a start. box_image_draft can read a box instead of files:
it lists what differs from the box’s image, such as the packages beyond
the image’s packages list and the tools in /usr/local, /opt and the
box user’s home, for the agent to turn into a script.
Building an image: a box, the script, a check, a capture
bb-box builds a project’s image with the access it already has: the
Proxmox API token, the docker CLI and SSH to the Mac. No backend needs
root on its host for it.
- A build box of the base kind, from the current standard image: not a thread’s and not enrolled. The panel shows it as “Building image rust-tools”. On Proxmox LXC it is a linked clone on LVM-thin, which is as independent as a copy, and a full clone elsewhere, so the standard template can still be replaced.
- The script, as root, with a time limit of 30 minutes by default. bb-box keeps its last 200 lines of output.
- Finalizing, as
finalize.shdoes, and the record. - The capture, with the box stopped:
- Proxmox LXC: the container becomes a template through the API, with the image’s name and build ID in its tags and the record in its description.
- Docker:
docker commit, with the record in a label. - Tart: the VM is renamed to the image’s name, as
prepare-image.shrenames its build VM, and the record is written beside it.
- The checks below, on a trial box. The new build replaces the last one only if they pass.
A project’s image is always built on the current standard image, never on its own last build, so its script runs once on top and rebuilds do not pile up layers.
Baked, at box start, or both
| Baked | Script at box start | Both: at start, then baked (chosen) | |
|---|---|---|---|
| A box’s wait | as a standard box’s; spares and warm guests work | the script’s time, every box: 22 s for the example, minutes with a dependency build | the script’s time for the first boxes on a backend, then as a standard box’s |
| A changed script | after a rebuild | at once | at once, at start, until the rebuild passes |
| Disk | one image per backend and kind | none | only for images in use |
| A failing script | fails the build; the last build stays | fails each acquire | as baked, once baked |
| The operator’s work | a build per backend | none | none |
- At start. A box of an image with no current build on its backend takes a box of the base kind, a spare if there is one, and runs the script as root before it is handed over. Acquire’s progress says “Running rust-tools’s script”. If the script fails, the box is released and the acquire fails with the script’s last 20 lines.
- Baked when used. bb-box bakes an image on a backend once two of its
boxes started there within 14 days. The build runs in the background, and
the next box after it starts as fast as a standard one. An image can say
always, to keep a build on every backend that offers its kinds, ornever, for a script that must run at each start. - A changed script makes the image’s builds out of date at once: new boxes run the new script at start, and a rebuild starts.
- Unused builds go. A build no box started from for 30 days is removed, and the next box runs the script again.
- Spares and warm guests can be kept per image once it is baked, as per kind now (032). None by default.
- Backends without images, such as the SSH host pool, always run the script at start.
Choosing an image
- A project’s image is its default. A thread in a project the image
belongs to gets it for the kinds it covers: from New box,
box_acquire,bb box acquire, and a thread started on a new box (033). - New box has an Image choice when an image covers the chosen kind: the project’s image first, marked “this project’s”, then Standard, then other images. BB’s new-thread inputs have the same choice.
- The agent’s tools.
box_acquiretakesimage(a name orstandard), andbox_listshows each kind’s images for the thread’s project. - A box says which it has. Its record and its details give the image and build, such as “rust-tools, built 2 October”.
The record
Every image, standard or a project’s, has /usr/share/bb-box/image.json,
and a copy that can be read without starting it: the LXC template’s
description, a Docker label, or a file beside the image on the Mac.
template-release and the packages list stay, for people.
{ "contract": 1, "image": "rust-tools", "kind": "linux-desktop-cpu", "arch": "x86_64", "build": "build.20261003-021500-3f9c", "built": "2026-10-03T02:15:00Z", "base": "build.20260928-020500-a1b2", "os": "Ubuntu 24.04.3 LTS", "aptSnapshot": "20260926T060000Z", "pins": "<SHA-256 of the pins it was built with>", "bbApp": "<SHA-256 of BB's host app>", "tools": { "node": "24.21.0", "claude-code": "2.1.283", "codex": "0.157.1", "pi": "0.87.1", "gh": "2.101.0", "chrome": "154.0.8037.92", "cua-driver": "0.28.3" }, "nvidia": null, "layer": { "sha256": "<SHA-256 of the script>", "seconds": 22 }, "sizeGiB": 3.3}base is the standard image’s build, for a project’s image. nvidia is
the userspace’s version where the image has one, as LXC GPU templates do;
Docker’s GPU desktops get it from the host.
When an image is stale
The doctor has a check for each image a backend uses.
| Finding | Status | What it costs |
|---|---|---|
| The contract is older than bb-box needs | fail | its boxes may not work |
| The NVIDIA userspace is not the host driver’s version, as the last GPU box saw it | fail | GPU desktops do not start (006) |
| No record: built before this decision | warn | nothing can be said of it |
| Older than twice its interval: 14 days for Linux, 60 for Windows and macOS | warn | tools and packages are behind |
| BB’s app is not the server’s | warn | each box downloads it as it enrols, 2 to 5 s more; spares hide it |
| Built with other pins than this bb-box’s | warn | it lacks what a bb-box update brought |
| A project’s image is on an older standard build, or its script changed | warn | rebuilt next |
| Its last rebuild failed | warn | with the reason and the output |
A check named “Nightly rebuilds” warns when none has run for 8 days.
Rebuilding at night
-
The job.
bb box images refreshrebuilds stale images one at a time, most needed first, and starts none after its time limit, 4 hours by default. It ends with a line per image, and exits 1 if any failed. -
Run by a BB automation, which the setup guide gives:
bb automation create --project <project> --name "bb-box images" \--cron "0 2 * * *" --timezone <zone> \--script "bb box images refresh" --timeout 4hBB lists its runs and output, and the operator can pause it, move it or run it now. If a script automation cannot run
bb box, bb-box schedules the same job with the plugin SDK’sschedule()instead. -
What is due, in this order:
- images that fail the doctor;
- standard images past their interval: 7 days for Linux on any backend, 30 days for Windows and macOS;
- project images whose standard image was rebuilt;
- Linux images with another build of BB’s app (a Docker rebuild changes only its last layer);
- images built with other pins.
-
How a standard image is rebuilt.
- Docker:
setup/docker/build.sh, as bb-box runs it now (042). - Tart: its prepare script, run on the Mac over SSH as the user bb-box logs in as, from a copy of bb-box’s setup directory.
- Proxmox LXC:
build.shneeds root on the host, which bb-box does not have, so bb-box refreshes the template from a box of it. The guest scripts run again with the current pins and BB’s app, a GPU template gets the NVIDIA userspace for the host’s driver from NVIDIA’s site, checked against NVIDIA’s SHA-256, then finalizing, a capture, and the template is replaced in place, keeping its VMID, asbuild.sh --replacedoes. A person runsbuild.shonly when the contract changes, which the doctor asks for. - Windows (039, 046): later, with PowerShell scripts.
- Docker:
-
Versions. A rebuild takes the latest stable release of the tools that change often and that bb-box does not depend on: Claude Code, Codex, pi, gh and Chrome, and an apt snapshot of that day. Each is checked against its upstream’s published SHA-256 or signature and recorded. Node, Cua Driver, the desktop stack and NVIDIA’s userspace stay pinned, and change with bb-box releases.
--pinnedbuilds from the repository’s pins alone. -
Room. A build starts only if its host keeps its disk reserve (
reserve-disk-gibon Proxmox LXC and Tart) with the build box, the trial box and the new image beside the old one, and has room for both boxes as for any box. A Mac that must keep 60 GB free sets its reserve to 60. Otherwise the image waits for the next night, and the doctor says why. -
Checks before replacing. All must pass on a trial box from the new build:
- the kind’s check and the image’s check, as the box user;
- the box answers within its kind’s ready time, with BB’s app at the recorded digest, and Docker answers on first use;
- for a desktop, the stream’s first frame, and a screenshot through Cua that is not blank;
- it answers no more than 5 s slower than a box from the old build;
- the record is whole, and no tool is older than in the old build.
-
Replacing. Proxmox LXC replaces the template in place, waiting on storage where boxes need it, as
build.shdoes. Docker moves the tag; the old image goes when no container uses it. Tart renames the new VM; boxes are copies on write and do not need the old one. Warm guests of the old build are retired as now, and idle spares are replaced. -
A failure keeps the old build and the output, and the image is not tried again until something it depends on changes or a person runs
bb box images build <name>.
To verify in the first PRs
- That the API token can make a container a template, remove a protected
one and clone into its VMID (
VM.AllocateandVM.Config.Optionson the pool), and that a description holds the record. - That
customize-base.shandcustomize-desktop.shgive the same packages and files when run again on a built template. - That a BB script automation can run
bb box. - Whether the token can create a container from Ubuntu’s container template with nesting. If so, bb-box can build headless and CPU desktop templates from nothing, without drift; GPU templates’ device lines need root, so they stay refreshes.
Consequences
- A project’s boxes start with its tools: the example project’s after 22 s for its first boxes on a backend, then as fast as standard ones.
- A person says what the project needs, or nothing; an agent writes and tries the script, often from files the project already has.
- Agent CLIs and Chrome in new boxes are at most a week old, and Windows and macOS images at most a month.
- The doctor says how old each image is and what is in it.
- More disk: one build per project image, backend and kind in use, full copies on Proxmox storage other than LVM-thin.
- Refreshing an LXC template from itself can drift from a fresh build. The
packages list shows it, and
build.shresets it. - A broken upstream release that the checks miss reaches new boxes until
the next build.
--pinnedand a pin in the repository hold it back. - Backends gain optional image operations: capture, replace, remove and list. Backends without them run scripts at start. The conformance suite builds a small image on each backend that has them.
Plan of PRs
- The record:
image.jsonfromfinalize.shand the Docker and Tart builds, the copies outside the image, and the doctor’s checks of contract, age, BB’s app, pins and the NVIDIA version. - Versions: the resolver for the tools that follow upstream, with their checksums.
- Image operations on Proxmox LXC, Docker and Tart, the build box and the trial box, and their conformance test.
- The nightly job:
bb box images refreshandbuild, the order, the room check, the output, the “Nightly rebuilds” check, and the setup guide’s automation. - Project images: the configuration,
box_image_set,bb box image setandremove, the script at start, baking when used and removing when unused. - Writing the script:
box_image_try,box_image_draftfrom files and from a box,bb-box-featurein standard Linux images, the skill’s flow, and New box’s offer for a project with no image. - Choosing: New box’s Image choice,
box_acquire’simage,box_list, and the box’s record and details. - Docs: the architecture, the setup guides, the kinds and the skill, with each PR.
Later: Windows and macOS scripts and their drafts, and builds that follow the script in the repository’s default branch without registering it again.