Developing bb-box
How to build and test bb-box, and how to add a backend. How bb-box works is in the architecture, and why in the decisions.
Before you change bb-box, read AGENTS.md: the layout, the
commands, and the rules every change follows. Coding agents read it first,
and CLAUDE.md points to it.
Build and test
npm installnpm run typechecknpm testnpm run buildnode scripts/smoke.ts <settings.json>npm run conformance -- --backend <type> --settings <settings.json>npm run reference- The tests and scripts need Node 22.18 or later, which runs TypeScript directly.
npm testruns against a fake Proxmox API, a fakessh, a faketarton a fake Mac and a fakedocker, so it needs no host. GitHub runs it, and the typecheck, on each pull request. It fails when the reference is out of date, a link between Markdown files is broken, or the skill does not name an agent tool.scripts/smoke.tsruns one box on a real backend without BB: it acquires a box, runs a command, opens a desktop as the relay does, and releases the box, printing each step’s time. Its settings file is the configuration asbb box config show --jsonprints it, with each secret as a string or{"file": ...};scripts/smoke.example.jsonreads the Proxmox LXC guide’s token file and key.--kind,--size,--commandand--doctorchoose what it does. Smoke runs are an install of their own,smoke, so they leave a real install’s boxes alone.npm run conformanceruns the conformance suite against a real backend.npm run referencewritesdocs/referencefrom the code. Run it after changing a tool, command, setting, kind or check.scripts/preview/renders the built panels outside BB, with BB’s theme and fixtures, for screenshots: serve the repository root and openscripts/preview/index.html#panel-list, or runnode scripts/preview/shoot.mjson a machine with Chrome.
Writing a backend
A backend is where bb-box gets boxes from. The interface it implements is in src/backends/types.ts and src/transports/types.ts, and the architecture explains it.
Where the code goes
| Part | Folder | Holds |
|---|---|---|
| Core | src/core/ |
the registry, lifecycle, sweep, in-box scripts, computer use and images. It never names a provider or transport |
| Transports | src/transports/ |
SSH, PowerShell, routes and openRoute, and the ready check. They never know which provider made a box |
| Backends | src/backends/<type>/ |
one driver per provider, plus what several share: ReadyPool and KvPoolStore in pool.ts, the Proxmox client in proxmox/, and what bb-box runs in a Windows guest in windows.ts |
The core uses only the two types.ts files.
Adding a backend
- Create
src/backends/<type>/with aBackendDriver:type, which is also the ID of the first backend of the type;label,descriptionand thekindsits backends can make;fields, andhostFieldsfor a backend whose hosts are set up alike: BB’s setting descriptors, each with a group (connection,credentials,boxesoradvanced), in the order the settings page shows them. A secret field is kept apart from the configuration;parse, which turns the backend’s part of the configuration into a typed config, or problems each at its field;setup, the path of its guide and of the scripts it runs, in order;several, for a type with one backend per host, as Docker has: the host a backend’s fields name (042);create, which builds theBackend. Its context gives the backend’s ID, its storage, its install ID, and its warm counts, kinds and hosts as the configuration has them now, so read them when needed. Where bb-box runs in BB,context.machinesalso reaches BB machines.
- Reuse a transport from
src/transports/, or add one there. A display served inside the box is built from the command transport’sroute(port). A kind with computer use needs a transport withspawn. - Record ownership at the provider (tags, labels, names or a marker file),
so
held()finds every guest even when bb-box’s storage is lost: the install ID, from the first write and on warm and part-made guests too, and the box’s ID, thread, name, kind and creation time, from which the sweep restores a lost record (018, 019). List only this install’s guests inheld(). - Write
doctor()so that it checks everything the backend needs and says how to fix each failure, with a stable ID per check. Add each ID toCHECKSinsrc/core/checks.ts: a check whose ID is not there does not compile, and the reference is made from that list. A backend with a warm pool givescontext.warmShortfalls()the pools it left short at the end of eachmaintain()(#356). GivesizesCheck()in src/backends/fit.ts each host’s cores and memory, so the doctor warns about a host too small for a kind’s default size. Where the backend’s images are rebuilt (049), give itfromCurrentBuild(), so the upkeep replaces a spare of a build that a refresh has replaced: false for such a guest, null when it cannot tell. It may be asked of a stopped guest, so answer without starting it. - Add the driver to
DRIVERSin src/backends/index.ts. - Add
setup/<type>/with the guide and its scripts. Keep deployment values out: use example addresses and names. Images remove BB’s daemon lock and PID file at boot (017). - Test it against a fake of the provider, as
test/proxmox-lxc.test.tsdoes withtest/fake-proxmox.ts, and run the conformance suite against it.
Nothing else changes: the registry, tools, panel and sweep pick up its kinds.
Rules the registry relies on
- Call
hooks.claim(guest)before making or changing anything. - If acquiring fails, release what was claimed before throwing.
- Make every method safe to repeat. A missing guest counts as released.
- Where the provider can reuse a reference, keep the box ID as
Guest.data.boxfrom the claim on, and check before acting that the guest at the reference still names this install and that box. If it does not,powerreturns null,releasedoes nothing, andstartandstopthrowgone. Addstamp()if your guests can predate install IDs. - Guests of another install, or with none, still count against capacity.
- A real backend calls
hooks.resetting()before it touches the machine, and puts the machine out of service if the reset fails. Keep the machine’s generation inGuest.data, so a repeated release of an old box never resets a machine the next box holds. - Give a resizable Linux or Windows desktop’s display
minSize: MIN_DESKTOP, 800x600. - Put what a person should know about a box, such as a desktop’s display
mode, in
Guest.facts. Likedata, they are never a secret. - Throw
BackendErrorwithno_capacity,unsupportedorgonewhen the registry should act on it. Any other error is shown as it is, so its message must make sense to a person and never hold a credential. - Offers of one kind must agree on what the agent relies on. The doctor
warns when they do not (
offerChecksinsrc/backends/index.ts).
The conformance suite
test/conformance/suite.ts runs the same checks against any backend:
- the doctor first, which must pass;
- for each kind: capacity, an acquire that records the guest before it changes anything, the transports the kind declares, a spawned channel, commands with stdin, exit codes and a timeout, the declared OS and architecture, streamed output, 32 MiB copied each way, what enrolment needs and enrolment itself with a live BB server, Docker, the iOS Simulator, each display and its resize, the display’s own input where its server ignores a viewer’s, desktop commands, computer use (the driver lists the windows, and the OS’s editor opens and shows text typed into it), stop and start (with the box’s clock right, and connected to BB again within 3 s), recovery by a new instance, and repeated release;
- for each preset: a box of that size, measured as soon as it is ready;
- interrupted acquires that leave nothing behind, a small image build, two installs on one backend that leave each other alone, and a reused reference that reads as gone.
Steps a kind does not declare are skipped by its capabilities, never by backend name, and any other skip gives its reason.
test/fake-backend.ts is an in-memory backend, virtual or real, that passes the suite and backs the core’s own tests. test/conformance.test.ts runs the suite against it, against Proxmox LXC with a fake API, against Tart on a fake Mac and against Docker on a fake Docker host:
node --test test/conformance.test.tsAgainst real hosts:
-
Docker, with the images built:
Terminal window echo '{ "backends": [{ "id": "docker", "type": "docker", "enabled": true, "fields": {} }] }' > /tmp/docker.jsonnpm run conformance -- --backend docker --settings /tmp/docker.json -
The SSH host pool, on a rig of Docker containers on one computer (its guide):
Terminal window sh setup/ssh-pool/test/rig.sh up /tmp/bbbox-rignpm run conformance -- --backend ssh-pool --settings /tmp/bbbox-rig/settings.jsonSSH_POOL_RIG=/tmp/bbbox-rig node --test --test-concurrency=1 test/ssh-pool-live.ts
The documentation site
site/ builds a documentation website with
Starlight from the Markdown in this
repository: the README, the user guide, the setup guides, the reference,
the skill, the architecture, this page and the decision records. It reads
them where they are, so a page added to or changed in docs/guide,
docs/reference or setup reaches the site with no change to site/.
Each section of the sidebar follows the order of the links in its index:
docs/guide/README.md, docs/reference/README.md, and the backend tables
in setup/README.md and the README. A page no index links comes last in
its section. A link to another file in the repository, such as a source
file, leads to it on GitHub. The site has its own package.json, so the
plugin’s install and build do not change.
To run it on your computer, with Node 22.18 or later:
cd sitenpm installnpm run devThen open http://localhost:4321. A page changes as you save its
Markdown.
npm run buildwrites the site, with its search index, tosite/dist. It also writes, for agents, llms.txt with a link to each page,llms-full.txtwith every page in one file, and each page’s Markdown at its address with.md, such as/guide/boxes.md, orindex.html.md, such as/guide/boxes/index.html.md.npm run check-linkschecks every link and asset insite/dist, in its Markdown too, and fails on one that leads nowhere or on a page llms.txt does not list.npm run previewserves the build, which search needs.
GitHub builds the site and checks its links on each pull request.
test/site.test.ts, part of npm test, checks without a build that every
guide, setup and reference page is on the site, in its sidebar and in
llms.txt, that every link on it and in its Markdown leads to a page or a
file, and that the home page’s setup prompt names only commands the CLI
has.
Publishing the site
The site is not published. It is static files in site/dist, so any
static host can serve it. Three settings, read when it builds, say where:
| Variable | Says | Default |
|---|---|---|
SITE_URL |
the site’s full address, for its sitemap and canonical links | none |
SITE_BASE |
the path the site is served under | / |
SITE_REPOSITORY_REF |
the branch that links to files in the repository use | main |
Two hosts suit it:
- GitHub Pages, as https://bbbox.dev is published:
.github/workflows/docs.ymlrunsnpm ciandnpm run buildinsite/on each push tomain, withSITE_URLset to the site’s address andSITE_BASEto/, checks its links, uploadssite/distwithactions/upload-pages-artifact, and publishes it withactions/deploy-pages. In the repository’s settings, under Pages, the source is GitHub Actions and the custom domain is the site’s address. A fork that publishes its own copy changes the workflow’s repository check and these two values. - Cloudflare Pages: connect the repository, with
siteas the root directory,npm run buildas the build command anddistas the output directory. SetNODE_VERSIONto 22.18 or later, andSITE_URLto the site’s address.
Recording the demo
Recording the demo says how to record the README’s demo video and the guide’s screenshots again, with the scripts in scripts/demo.