Skip to content

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

Terminal window
npm install
npm run typecheck
npm test
npm run build
node 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 test runs against a fake Proxmox API, a fake ssh, a fake tart on a fake Mac and a fake docker, 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.ts runs 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 as bb box config show --json prints it, with each secret as a string or {"file": ...}; scripts/smoke.example.json reads the Proxmox LXC guide’s token file and key. --kind, --size, --command and --doctor choose what it does. Smoke runs are an install of their own, smoke, so they leave a real install’s boxes alone.
  • npm run conformance runs the conformance suite against a real backend.
  • npm run reference writes docs/reference from 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 open scripts/preview/index.html#panel-list, or run node scripts/preview/shoot.mjs on 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

  1. Create src/backends/<type>/ with a BackendDriver:
    • type, which is also the ID of the first backend of the type;
    • label, description and the kinds its backends can make;
    • fields, and hostFields for a backend whose hosts are set up alike: BB’s setting descriptors, each with a group (connection, credentials, boxes or advanced), 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 the Backend. 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.machines also reaches BB machines.
  2. Reuse a transport from src/transports/, or add one there. A display served inside the box is built from the command transport’s route(port). A kind with computer use needs a transport with spawn.
  3. 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 in held().
  4. 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 to CHECKS in src/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 gives context.warmShortfalls() the pools it left short at the end of each maintain() (#356). Give sizesCheck() 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 it fromCurrentBuild(), 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.
  5. Add the driver to DRIVERS in src/backends/index.ts.
  6. 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).
  7. Test it against a fake of the provider, as test/proxmox-lxc.test.ts does with test/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.box from the claim on, and check before acting that the guest at the reference still names this install and that box. If it does not, power returns null, release does nothing, and start and stop throw gone. Add stamp() 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 in Guest.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. Like data, they are never a secret.
  • Throw BackendError with no_capacity, unsupported or gone when 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 (offerChecks in src/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:

Terminal window
node --test test/conformance.test.ts

Against real hosts:

  • Docker, with the images built:

    Terminal window
    echo '{ "backends": [{ "id": "docker", "type": "docker", "enabled": true, "fields": {} }] }' > /tmp/docker.json
    npm 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-rig
    npm run conformance -- --backend ssh-pool --settings /tmp/bbbox-rig/settings.json
    SSH_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:

Terminal window
cd site
npm install
npm run dev

Then open http://localhost:4321. A page changes as you save its Markdown.

  • npm run build writes the site, with its search index, to site/dist. It also writes, for agents, llms.txt with a link to each page, llms-full.txt with every page in one file, and each page’s Markdown at its address with .md, such as /guide/boxes.md, or index.html.md, such as /guide/boxes/index.html.md.
  • npm run check-links checks every link and asset in site/dist, in its Markdown too, and fails on one that leads nowhere or on a page llms.txt does not list.
  • npm run preview serves 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.yml runs npm ci and npm run build in site/ on each push to main, with SITE_URL set to the site’s address and SITE_BASE to /, checks its links, uploads site/dist with actions/upload-pages-artifact, and publishes it with actions/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 site as the root directory, npm run build as the build command and dist as the output directory. Set NODE_VERSION to 22.18 or later, and SITE_URL to 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.