Architecture
How bb-box works now: its parts, the backend interface, a box’s life, commands and jobs, desktops and control, images, the relays through BB machines, the configuration, and each backend. It describes the current design only. Why each choice was made, and what came before, is in the decisions. The exact tools, commands, settings and checks are in the reference, made from the code.
The rule
bb-box’s core does not care who the provider is (001). A hypervisor, a container host, a pool of physical machines or a cloud is a backend behind one interface, and one conformance suite tests every backend against it.
Parts
BB app: Box panel, Boxes page, desktop and terminal tabs, settings BB server | | v v+------------------------------ bb-box plugin, on BB's server -------------------------------+| Core: registry, lifecycle, sweep, agent tools, CLI, views, configuration, doctor, || desktop relay, shared control, enrolment, computer use, images, in-box scripts ||--------------------------------+-----------------------------------------------------------|| Transports | Backends || command: SSH, docker exec | proxmox-lxc, proxmox-qemu, docker, microsandbox, || routes: tcp, command, | ssh-pool, tart, hetzner, namespace || websocket | |+--------------------------------+-----------------------------------------------------------+ | | | v v v inside each box each provider's API or CLI bb-box's host entry on BB machines: access, the Docker and msb relays, file streams| Part | Folder | Owns | Never |
|---|---|---|---|
| Core | src/core/ |
box records, lifecycle, sweep, computer use, the desktop relay’s protocol work, images, in-box scripts | names a provider, hypervisor, container technology or transport |
| Plugin | src/plugin/ |
what BB calls: tools, CLI, RPC, HTTP routes, setup, enrolment, the nightly rebuilds | |
| App | src/app/ |
the panel, the Boxes page, the desktop and terminal tabs, the settings page | |
| Transports | src/transports/ |
running commands, copying files, opening a route to a port in a box | knows which provider made the box |
| Backends | src/backends/<type>/ |
acquire and release, capacity, ownership records, which transports and displays reach each box, images, doctor |
runs anything in a box for the core |
| Host entry | src/host/ |
what BB’s daemon runs for bb-box on a BB machine | |
| Relays | src/relay/ |
the Docker and msb relays and file streams, as channels on one WebSocket |
- bb-box is a BB plugin with no service of its own (002). It calls each provider’s API or CLI from BB’s server.
- What runs inside a box is written once per guest OS and sent through the command transport: POSIX sh for Linux and macOS, PowerShell 7 for Windows. That covers enrolment, the thread’s access, commands, file copies, jobs, computer use and a real machine’s reset.
- Several backends can be on at once, and several can offer the same kind.
The backend interface
The types are in src/backends/types.ts and src/transports/types.ts, and the code is their reference. Writing a backend says how to build one.
A backend declares its nature, virtual or real, and implements:
| Method | Does |
|---|---|
kinds() |
the kinds it offers, with their capabilities |
capacity(kind, size, pending) |
whether one more box of that size fits, with each host’s limits and a message that says what is full |
acquire(box, previous, hooks, signal) |
makes or claims a guest, starts it, and returns when its command transport answers, within the kind’s readyTimeoutMs |
release(guest, hooks) |
deletes a virtual guest, or resets a real machine and returns it to the pool |
power(guest) |
running, stopped or unknown now, from the provider, not a listing that may lag; null when the guest is gone |
start?, stop? |
only where the provider can; the panel and tools offer them only then |
stopped?(guest) |
why a guest stopped when bb-box did not stop it |
held() |
every guest this install holds, with the owner recorded at the provider |
stamp? |
records this install on a guest from before install IDs |
relabel? |
records a box’s new owner or name on its guest, as when a spare is claimed |
asOffered?(guest, kind, createdAt) |
whether a guest comes from what the kind is offered from now |
footprint?() |
what its boxes take that another backend’s must not, such as guest IDs and addresses |
transports(guest, kind) |
the command transport, the displays, and how the thread reaches the box |
images? |
the standard images it builds, and builds of projects’ images |
doctor() |
checks the setup end to end and says how to fix what is wrong |
maintain?() |
upkeep, such as topping up a warm pool, at each sweep and when the configuration is applied |
members?(), returnToService?(ref) |
a real pool’s machines and their state; puts back a machine the operator fixed |
A real backend also says what a release restores: home, the box user’s
home, or machine, the whole machine.
- Every method is safe to repeat: the registry resumes after a restart and the sweep repeats releases. A missing guest counts as released.
- A backend records the guest with
hooks.claimbefore it makes or changes anything, and releases what it claimed when an acquire fails. - Each guest names its install and its box at the provider
(018). Where a
provider reuses references, as Proxmox reuses VMIDs,
power,start,stopandreleasefirst check that the guest still names the box inGuest.data.box. Otherwisepowerreturns null,releasedoes nothing, andstartandstopthrowgone.
Kinds and offers
A kind is a shared word for what a box promises. An offer is a backend and a kind. The registry tries a kind’s offers in the configuration’s order of backends, and the first with room takes the box.
| Kind | Promises | Offered by |
|---|---|---|
linux |
Linux on x86_64, no GPU | Proxmox LXC, Docker, microsandbox, an SSH pool’s Linux machines, Proxmox QEMU as VMs, Hetzner Cloud’s hosts, Namespace |
linux-desktop |
a Linux desktop on a GPU | Proxmox LXC on a GPU host, Docker with NVIDIA’s container toolkit |
linux-desktop-cpu |
a Linux desktop drawn by the CPU | Proxmox LXC, Docker, Hetzner Cloud’s hosts, Namespace |
linux-desktop-wayland-cpu |
a Linux desktop on Wayland, drawn by the CPU; X11-only apps do not run | Proxmox LXC, Docker, Hetzner Cloud’s hosts, Namespace |
linux-arm64 |
Linux on arm64 | Tart |
linux-arm64-desktop-cpu |
a Linux desktop on arm64 | Tart |
macos |
macOS | Tart, an SSH pool’s Macs, Namespace |
macos-desktop |
a macOS desktop | Tart, an SSH pool’s Macs, Namespace |
windows-desktop |
a Windows desktop on x86_64 | Proxmox QEMU |
windows-arm64-desktop |
a Windows desktop on arm64 | Tart’s Macs, in QEMU |
- Names. A kind without a desktop has no qualifier, and a desktop kind
says
desktop(027). A kind names its architecture when it is not the OS’s usual one, solinuxis never arm64 (040). The IDslinux-headlessandlinux-headless-dockerare still accepted where an ID comes in, and core maps them tolinux. - Docker.
linux-dockerandlinux-arm64-dockerare aliases oflinuxandlinux-arm64that take only offers with the featuredocker(041). Proxmox LXC boxes and Tart’s Linux VMs have Docker. Docker boxes have it unless the backend’sprivilegedfield is off or the host is rootless Docker, and a GPU desktop has it when its image was built after #283. An SSH pool has it only when every machine has a whole-machine reset. An offer or box without it says “No Docker”. - VMs.
linux-vmandlinux-kvmare aliases oflinuxthat take only an offer with the featurekernelorkvm(057): Proxmox QEMU’s VMs, and forkvmNamespace’s Linux boxes too, each a privileged container on a machine of its own whose/dev/kvmevery user may open. A plainlinuxacquire tries such an offer last, as it starts slower and holds its memory or costs by the minute, andbox_listlists it last. Proxmox QEMU offerskvmonly when itslinux-kvmsetting says the host allows nested virtualization. - Agreement. Offers of one kind agree on what the agent relies on:
os,arch,gpu, the computer-use driver,enrol, whether there is a desktop, and the features the kind requires. An offer may add features, such as a pool Mac’sxcodeandios-simulator. The doctor warns when offers disagree (012). - One provider. An acquire may name its provider with
on: asbox_listgives it, by its backend ID, or by its one host’s name. A list of them takes the first with room among them. Then only those are tried, and a full or offline one is refused rather than passed over (044). - GPU. “No GPU” means no scarce GPU is taken, so macOS kinds declare
gpu: false. Alinux-desktopbox whose display fell back to the CPU does not meet its kind: the backend releases it and tries another GPU within the kind’s time, and new desktops avoid that GPU for 10 minutes (020). Proxmox’s API cannot read GPU memory, so the Proxmox LXC backend reads it from a report the host writes: a root timer writesnvidia-smi’s figures in the notes of a VM that is never started, which the token may read and not change. Without a report under 10 minutes old, it keeps each GPU’s free memory as a desktop on it last read it: its display service as it starts, andnvidia-smiin a running one during upkeep, for 5 minutes. A new desktop skips a GPU with less than it needs, and is refused before it starts when every GPU has less (#603). - Time to ready.
readyTimeoutMsis 30 s for a Proxmox LXC box, 60 s for a Docker box or a Linux pool machine, 3 minutes for a Windows VM or a pool’s Mac, and 5 minutes for a Tart macOS or Linux VM.
Capabilities
| Capability | Values | The core uses it to |
|---|---|---|
os |
linux, windows, macos |
pick the in-box scripts |
arch |
x86_64, arm64 |
tell the agent what runs there |
gpu |
yes or no | show GPU status and count GPU capacity |
features |
words such as docker, xcode, ios-simulator |
let the agent choose, and show each box’s extras |
command |
transport (a name for people), shell (sh or powershell), admin, inDesktopSession, streaming; or none |
offer commands and file copies |
displays |
{id, protocol, resizable} for each display; none without a desktop |
load the viewer client, and allow resizing |
computerUse |
{driver, version, socket}, or none |
offer the computer tools (015) |
enrol |
yes or no | make the box a BB machine |
- A kind enrols only when its shell is
shand its OS is Linux or macOS. BB’s host app does not run on Windows yet (BB requests). - Computer use needs a command transport with
spawn, which keeps a command running with its stdin and stdout as a byte channel. - A box records its kind’s capabilities when it is made. When the plugin
starts, and after the configuration changes, core gives each box what its
kind has now, where the backend’s
asOfferedsays the guest comes from what the kind is offered from now. Proxmox LXC answers by the template’s build ID; Docker, Tart and pool kinds have capabilities fixed in code.
Reaching inside a box
transports(guest, kind) returns the command transport, the displays, and
access: how the thread’s own machine reaches the box, by SSH or a Docker
host, or null when only BB’s server can.
Routes. A route says how to reach one port inside a box, as data:
| Route | Holds | For example |
|---|---|---|
tcp |
host and port | a box on a network BB’s server reaches |
command |
a command whose stdin and stdout carry the bytes | ssh -W 127.0.0.1:5900 <box>, docker exec -i <container> socat - TCP:127.0.0.1:5900 |
websocket |
URL, headers, and a certificate fingerprint or CA | Proxmox’s vncwebsocket with its one-time ticket |
openRoute in src/transports/route.ts is the only code that opens one.
A route is made for each connection, as it may hold a one-time secret, and
is never logged or sent to the browser. A command transport’s route(port)
gives a route to a port inside the box: ssh -W, or docker exec with
socat.
Displays. A display has an ID, a protocol (rfb, or selkies beside
it on an X11 CPU desktop), whether it can resize, and open(), which
returns a route and how to sign in: none, a VNC password, or a user and
password for Apple’s authentication. A display may also have:
resize(), which the relay calls when the stream server cannot resize (021);minSize, 800x600 on Linux and Windows desktops: the relay raises a smaller size to it, and the viewer scales the desktop down;input, for a stream server that ignores a viewer’s keys and pointer (#406): the relay sends the controlling viewer’s input there instead;clipboard: "commands", for a server that does not move the clipboard: bb-box moves it with commands in the desktop session (#464).
SSH. SSH access gives the host, port, user, host keys and an optional jump host: the Mac for Tart, a bastion for a pool (016). A real machine’s host keys are pinned at setup; a fresh virtual guest’s key is accepted and not recorded. Commands to one box share a connection where the backend gives the transport a socket directory, as Proxmox LXC does: at most 8 commands on it at once, retired after two idle minutes, a failed command or a missing socket, with keepalives every 5 s. A probe always dials afresh.
Guests, capacity and sizes
- Guests. A guest is the backend’s handle on a box’s machine: its
reference, its host, its address, its size, and
data, small values the backend needs later, stored with the box and never a secret.facts, such as a desktop’s display mode, show in status and the panel. A backend with several hosts set up alike lists them in its configuration, andhostnames one; each Docker host is a backend of its own. - Capacity. A box needs its memory, disk and a slot on one host, so
capacityreports named limits per host, plus the warm pool for the kind:memoryanddiskin MiB;cpu;guests(Tart’s two macOS VMs per Mac, a Docker container budget, Proxmox’s VMID range);gpu-desktopsandgpu-memoryper GPU; andmachinesfor a real pool. Each limit has what is used, the total if known, and a reserve for the host. - Sizes. A box’s size, cores, memory and root disk, is chosen when it
is made (028).
Each kind has the presets small, medium and large, a default and a
largest custom size, the same on every offer and changed in the
configuration’s
kindspart. Core refuses a size above the largest, and a Linux size under 2 GiB of memory. An offer that cannot make a size says so, and the registry tries the next. A pool lends its smallest free machine of at least the size, counting 90% of the memory. - Real pools.
releaserecords “being reset” withhooks.resetting()before it touches a machine, so a restart finishes the reset. The machine keeps a reset generation, and the box keeps its machine’s generation, so a repeated release of an old box never resets the machine the next box holds. A claim checks the machine is clean first, and puts it out of service with the reason if not.
A box’s life
requested -> acquiring -> starting -> ready -> enrolled | ^ stop | | start (you or the agent) v | stopped
any state -> releasing -> released (release, or the thread is archived or deleted)- Requested. The registry asks each offer of the kind for
capacityat the box’s size, in order, and uses the first with room. Otherwise the caller gets the backend’s message, which says what is full. The record is made at once. - Acquiring. The backend claims a guest with
hooks.claimbefore it makes anything, so a restart resumes and the sweep finds it. - Starting. The guest boots. Each step’s time goes to
hooks.timingand shows in the box’s status. - Ready. The command transport answers. A box of a project’s image that has no build on its backend runs the image’s script now, before it is handed over (049).
- Enrolled. A box whose kind can enrol becomes a BB machine, and the thread’s access reaches it.
- Released. You or the agent release it, or its thread is archived or deleted (003). bb-box removes the machine from BB, then the backend releases the guest. A box a thread is working on is released or stopped only when forced (034). Each release logs why.
Stopping keeps the disk and frees memory or a GPU. It never happens on a timer: boxes do not expire.
| Virtual | Real | |
|---|---|---|
| Acquire | clone and start a guest, or take a warm one | claim a free machine and make its box user |
| Ready | the transport answers | the transport answers as the box user |
| Stop and start | where the provider can | no |
| Release | delete the guest | remove the box user and what it left, or restore the whole machine |
| If release fails | the sweep retries | the machine goes out of service until the operator fixes it |
Warm guests and spares
- Warm guests are ready guests of a kind that are not BB machines, so
an acquire is only a claim (004).
ReadyPoolin src/backends/pool.ts picks a member that can serve the kind. The count is per backend and kind in the configuration, 0 by default.maintain()tops pools up after a claim, when the configuration is applied and at each sweep, never below the backend’s reserves, and reports a pool it left short, with the reason, which the log, the doctor and the settings page show (#356). Warm guests wait at the kind’s default size, from a build that is current: one from another template build, image build or default size is retired. How each backend keeps them is under Backends. - Spares are boxes made ahead and enrolled, at a kind’s default size,
that no thread owns yet: up to 10 per kind
(032). An acquire at
the default size takes one, so the box has the thread’s access at once.
BB lists a spare as “
(spare)”. When no backend has room for a thread’s box, bb-box releases a spare to make room. A spare that a thread starts on through BB’s picker becomes your machine (036). The upkeep replaces a spare, ready or waiting stopped, that its backend’s fromCurrentBuildsays comes from a build of the kind’s image that a refresh (049) has replaced since, so a claim never takes an old build, and never waits on the backend for it. Proxmox LXC and QEMU compare the build recorded at the clone with the template’s, Docker the container’s image ID with the tag’s, microsandbox the image record a microVM took at creation with the image’s, and Tart the build of its image a cloned macOS VM recorded with the image’s record on its Mac. The SSH host pool cannot tell, as a pool machine has no build, and nor can a Tart box made from a warm VM or before builds were recorded, so those spares stay until they go otherwise. - Spares of device boxes (063, unit 7.5) are off by default: an
operator sets a count for ios or android. A device spare stops to wait
only once its device server says the device has booted, through the
plugin’s
spareSettled; one that has not booted 10 minutes after it was ready is released, and the kind makes none for 30 minutes. A claim tells the plugin throughspareClaimed, which switches the device to the model the thread asked for, with the server’s own rollback. A spare that waits stopped longer than its backend’ssavedSessionRestoresWithinMsis released and replaced: Tart’s iswarm-max-hours, as a saved macOS VM older than that may not restore (063 results, E4). - Spares of projects’ images. Where a project’s image is baked, on
Proxmox LXC or Docker, bb-box also keeps one spare of the build for each
kind, while the image is in use: a box started from the build, or the
build was made, within 14 days, or the image says
bake: always. The image’s boxes claim it, and a new build replaces it (#440).
Enrolment and the thread’s access
A box is a resource of its thread and gets what the thread’s machine has.
- Enrolment. bb-box is a BB machine provider. It creates a machine
keyed by the box ID, and the provider runs BB’s
bootstrapthrough the box’s command transport (014). Images carry BB’s host app, so the installer skips its download; a box enrols about 5 s after it is ready on Linux, and 8 to 11 s on a Mac (BB request 5). Images clear BB’s daemon lock and PID file at boot (017). An enrolment fails, rather than counting the box enrolled, when BB does not show the machine connected within 30 s (#286). A join that failed for something that can pass, such as a DNS lookup that timed out, is tried again after 30 s, 2 min, 10 min and 30 min, then every 30 minutes, and gives up 24 hours after the first failure (src/core/join-tries.ts). Its words blame BB’s setup only when no box on the same backend reaches BB. The sweep plans the same tries for a box whose record says such a failure but has no tries, as one that failed before bb-box retried them.bb box enrol,box_enroland the box page’s Join BB again add a ready box to BB again. Each try, these and the sweep’s, first asks BB about the machine the record names: one BB still has makes the box enrolled, and is never removed. Otherwise they run BB’s installer through the box’s command transport, never the backend’s start, stop or resize, and stop only the BB daemon of a machine BB no longer has. The sweep plans tries only for boxes bb-box enrols. - Variables. BB gives the thread’s Git credentials and machine
variables to its daemon in the box. Each command bb-box runs, and each
SSH session through the box’s
~/.bashrc(~/.zshenvon macOS), takesGH_TOKEN, the git identity and BB’s machine variables from a process of that daemon as it starts: from/procon Linux, fromps -Eon macOS, and through each process’s PEB on Windows, on trial, for bb-box’s own commands only (#560; BB request 8). The values never reach the disk. bb-box’s host entry keeps a process with the current values running in a worker of the daemon (src/host/access.ts), so commands always find one. A project’s own variables reach a box only while a thread of the project runs there (BB request 11).gittakes GitHub credentials fromgh, and fetches GitHub’s SSH URLs over HTTPS. - Child threads. A child thread can run on an enrolled box
(
box_thread), with its parent’s agent and model when the box has that agent’s CLI (#561). BB’s new-thread screen can start a thread on a new box, which goes with the thread (033). - Your machines. Boxes you own, kept as BB machines under names you give, until you remove them (036).
- Files for every box. A short list of files, each a path under the box user’s home and a secret content, written into each Linux and macOS box as it joins BB, starts again or the list changes (#455). Windows boxes get none yet.
- Direct access. The agent can reach its boxes itself, with
ssh <box name>orDOCKER_HOST, whereaccessallows. - Windows boxes enrol as machines of BB’s manual provider, which bb-box makes and removes with the box (#539), as BB enrols a provider’s machine with its installer for Linux and macOS only. A switch on each backend with Windows boxes turns it off.
Clean-up and crash recovery
Nothing expires, so no box may be forgotten.
- Ownership is recorded at the provider: Proxmox tags, Docker labels,
an owner file per Tart VM, a marker file on a real machine.
held()reads it back, so the sweep works even when bb-box’s storage is lost. - Each install records its ID on every guest and acts only on its own.
An install that lost its storage takes its old ID back with
bb box install-id --set(018). - After a restart the registry resumes acquires, repeats interrupted releases and checks the power of the rest.
- The sweep runs at start and every 3 minutes. It records the install
on older guests (
stamp()); releases the boxes of threads that BB says ended, at this sweep and the last, since one answer may be wrong (019); finishes interrupted releases; restores the record of a guest whose thread is live and releases one whose box holds another guest or none; and callsmaintain(). A restored box enrols again, keyed by its box ID, once it answers (026). It changes a box’s state only whenpower()confirms it, and logs each guest it releases, restores or keeps. - A guest deleted outside bb-box shows as gone, and stays gone when its reference is reused.
- A guest that stopped without bb-box’s request is recorded with what
stopped()says. When the guest did not choose it, as when its VM crashed, bb-box starts the box again, unless it did so less than 10 minutes before (#406). - A release is done only when the backend confirms it (069). The box log records “release asked” before the backend is called and “released” after it confirms; the record goes only then. A release that fails keeps the record, with the backend’s words, and the sweep tries again. After a restart in between, the sweep first asks for the guest’s power, and a guest that is gone finishes the release with no second call.
- A box whose backend is no longer in the settings can be forgotten
(
bb box forget): bb-box removes its record and deletes nothing. - A box that does not answer a command, a Live reading or a view is checked again by itself after 1 minute, 5 minutes, then every 15 minutes, through BB’s connection state for a BB machine and otherwise one command with a 10 s timeout. The check never calls a backend’s API; Check now reads the box’s power, at most once a minute (069).
- A box that rebooted starts its services again, and the sweep bootstraps its BB machine again when BB shows it disconnected.
- If BB’s server is down, boxes keep running and nothing is released.
Commands and jobs
- A run ends when the script’s shell exits, even while something it
started holds the output open.
CommandLinein src/transports/process.ts runs a POSIX script in a child shell, then writes an end mark and the exit status, so a background process cannot keep the run open.box_runkeeps the first and last 16 KiB of each stream. - Windows. src/transports/powershell.ts
sends the script base64-encoded, and exits as sh would. A script longer
than Windows’ command line goes to a file first. sshd ends everything a
command started when it exits, so
Start-Detachedstarts a command outside the session. - The desktop session.
box_runwithdesktopruns a command in the logged-in desktop session: withDISPLAYand the session bus on Linux, throughlaunchctl asuseron macOS (030), and as an unelevated one-off scheduled task on Windows, whose output comes back when it exits (039). - Jobs. BB ends a tool call at about five minutes, so
box_runreturns after 270 s, and a command still running goes on in the box as a job, whichbox_jobreads. Each command runs as a job from the start, with its output and exit status in files under~/.local/state/bb-box/jobs/, so it also outlives a restart of bb-box (#405): bb-box’s dispose hook waits up to 60 s for running commands, then leaves them to their jobs. On Windows a runner started withStart-Detachedholds the job, outside the SSH session (#414). A Windows command in the desktop session has no job, and stops at 270 s. A box’s next command removes the jobs bb-box has the result of, and those older than 7 days. - Files.
box_copymoves one file between the thread’s workspace or storage folder and a box. A file up to 16 MiB goes through BB’s file API. A larger one streams through the file relay, and the box and bb-box’s worker each check its SHA-256 before it replaces the target (#492). In a box, file copies are scripts with the file’s bytes as their stdin or stdout. - The terminal. A person’s terminal is the box user’s login shell over
the command transport, with its PTY on BB’s server, on the plugin’s
/terminalroute (035). It lives 10 minutes without a tab.
Desktops and control
The desktop relay. Each viewer opens one WebSocket on the plugin’s
/desktop route (007).
The relay opens the display’s route, does the box’s RFB security
handshake itself, VNC authentication with the box’s password or Apple’s
(type 30) as a user, then offers the viewer RFB 3.8 with security type None
only, as BB has authenticated the viewer. Passwords and tickets never reach
the browser. It copies bytes both ways without decoding pixels, and always
joins as a shared client. The viewer is noVNC.
The Selkies relay. An X11 CPU desktop also has a Selkies display, H.264
over WebSockets from 127.0.0.1:5980 in the box
(056).
A viewer asks for it with &stream=selkies. With the general setting
video on, the panel shows such a desktop as video when its fastest of
three round trips to BB’s server takes 10 ms or more, and over RFB when it
is quicker, as RFB answers keys faster on a LAN. The viewer’s menu has
“Show as video”, and the tab keeps that choice for the box. Video needs
WebCodecs, so a browser without it, or video that does not start, shows
RFB. The panel’s client decodes the H.264 with WebCodecs onto a canvas,
takes keysyms from noVNC’s keyboard handling, and stops the video while its
tab is hidden. The relay speaks WebSocket to Selkies over the display’s
route and carries messages, not bytes:
- Each viewer joins Selkies as a viewer (
role=viewer), so Selkies never hands the display from one page to another. The relay passes only its video requests, and drops the rest of what it sends. - The input of the viewer with control goes over the box’s input connection, one per box, which asks for no video. It opens as a person takes control, so their first key does not wait for it. A pointer loses its sequence number there, as several viewers’ input shares it.
- The box’s clipboard reaches only the viewer with control.
- Selkies paces a viewer by its socket, which the route hides, so the relay paces each by the frames it acknowledges: a second behind, it drops that viewer’s frames until it catches up, then asks for a key frame, which goes to every viewer, and resumes from it. It asks on the input connection when that is open, as Selkies takes a viewer’s request once a second, and again every 250 ms until one comes.
- Every relay runs on the event loop of BB’s server, whose process runs
bb-box, so code there that holds the loop holds every desktop’s frames
and input, whether it is bb-box’s, BB’s or another plugin’s. bb-box logs,
at most once a minute, when the loop stood still for 250 ms or more, how
much of it was garbage collection, and how many processes bb-box started
then and how long their spawn calls took; and five minutes after a start,
then once an hour, the server’s memory, how many processes the helper
started, and which started the most, each by where bb-box started it and
the work it was for, such as “ssh command for sweep” or “docker image for
tool box_acquire”. Forking copies the parent’s page tables on the loop’s
thread, about 49 ms a spawn for a 3.5 GB server, so bb-box’s commands,
Docker’s and msb’s, and its ssh connections’ masters, start from a small
helper process, started once, which forks for them and passes their
streams back over its IPC channel (
src/transports/forker.ts); a spawn call then takes about 0.1 ms. Desktop routes and terminals, which live long, still start directly. Should the helper end, bb-box spawns directly for 30 s, then starts it again; when bb-box stops or reloads, the helper ends with its children.BB_BOX_DIRECT_SPAWN=1turns it off. A CPU profile of the process, 1 to 60 s, one at a time, names the code (bb plugin rpc call bb-box profilewith{"seconds": 15}), splits the busy time by whose code it ran under, notes each long garbage collection with the heap after it, and times starting a process. It logs only functions’ names, files, lines and times. With"keep": trueit keeps the profile, with"allocations": trueit samples who allocates, and with"snapshot": trueit writes a heap snapshot, which holds the server still while it is written and is refused without the free memory for it; each goes toprofiles/beside bb-box’s own database in BB’s data, the last five. With"collect": true, after the profile it has V8 collect all the garbage, and says how long that held the server and the heap’s used size before and after, so the memory line that follows is what the process keeps, not garbage not yet collected. A full collection of a large heap holds the server for hundreds of milliseconds, so it is for an occasional sample. - Selkies refuses a connection within 500 ms of another from the same address, and the relay’s all come from the box’s loopback, so it spaces them and tries again.
- A resize from the viewer with control goes through the RFB display, at a width of a multiple of 8 and an even height, as Selkies keeps them. Selkies follows a desktop that shrinks, but captures one that grows at its old size, even for later viewers. So after each resize bb-box makes, and when a viewer’s frames have another size than the desktop’s, the input connection sends SETTINGS with the desktop’s size as a manual resolution, which moves the capture at once, without a restart. A capture moved to a larger size keeps its encoder’s old size, so the picture is cut from the desktop’s top left. So when the desktop grew past its video’s size, every viewer’s video stops for 300 ms: Selkies stops a capture no page wants, and starts the next at the desktop’s size, from a key frame.
Each desktop’s server.
| Desktop | Server | Signs in with | Resize |
|---|---|---|---|
| Linux, X11, GPU | TigerVNC’s x0tigervncserver on Xorg |
VNC password | the backend, with xrandr --fb |
| Linux, X11, CPU | Xtigervnc |
VNC password | the server |
| Linux, Wayland | wayvnc on Sway (048) | type 30, as box |
the backend, with the display script’s resize |
| Windows on Proxmox | Proxmox’s console, through vncwebsocket |
a one-time ticket and password | the backend, in the desktop session |
| Windows on a Mac | QEMU’s VNC on the Mac’s loopback | none | the backend, in the desktop session |
| macOS on Tart | the Virtualization framework’s VNC, through a holder on the Mac | VNC password | none |
| macOS on a pool’s Mac | Screen Sharing, through SSH as the box user | type 30, as the box user | none |
- A frozen sign-in. wayvnc 0.7.2 can freeze at a viewer’s sign-in right after a resize with no viewer has restarted it, until it crashes seconds later (#411, about 0.7% of such resizes). A display that can restart its server, as a Wayland desktop’s can, gets 4 s for the sign-in; then bb-box kills its wayvnc, the desktop starts the next, and the sign-in is tried again once, within the usual 20 s.
- The picture. The viewer asks a display for JPEG quality 2, or the
quality the display names, and keeps noVNC’s compression level, 2
(056,
measured). Xtigervnc sends a still area again
near losslessly about 0.1 s after it stops changing. wayvnc never does,
and QEMU with
lossy=onpartly as JPEG still. So for a display withrefresh: "viewer", the Wayland desktops and Windows on a Mac, the viewer asks for the area it drew as JPEG again with no quality level once nothing has come for 250 ms, then for JPEG again (src/app/rfb-refresh.ts). Windows on a Mac names quality 4, as QEMU’s JPEG at 2 breaks moving text up. Proxmox’s console and Tart’s holder send no JPEG. - A pool Mac’s input. macOS 27’s Screen Sharing ignores a viewer that
is not Apple’s, so the display’s
inputis core’sMacInput: a JXA helper in the box user’s session that posts each event withCGEventPost, keys by their position on a US keyboard (047). - The clipboard. Cmd+V, or Ctrl+V off a Mac, sends the browser’s
pasted text as ClientCutText and presses the box’s paste chord. The
viewer sends this computer’s clipboard only with a paste, never on
taking control or the focus (F-15). What the
box copies comes back as ServerCutText, to the viewer with control only.
TigerVNC and wayvnc move it both ways. For a display with
clipboard: "commands", Tart’s macOS desktop and both Windows desktops, bb-box moves it itself for the viewer with control:pbcopyandpbpaste, or PowerShell’sSet-ClipboardandGet-Clipboard, in the desktop session. The viewer asks for the box’s clipboard every 2 s while its tab has the focus, one read per box at a time (#464, #461). - Viewers. Each viewer has a random ID kept in its tab, so a tab that remounts keeps its control. Control belongs to one viewer, not to a person (020). The app keeps a hidden tab’s desktop and terminal for 10 minutes, and a hidden tab that holds control hands it back after 60 s (037).
- Shared control. One owner of input at a time, the agent by default (008). The relay drops keys, pointer, clipboard and resizes from every other viewer, and passes update requests, so all see the desktop. On hand-back it releases what was held down and sets the desktop back to the agent’s size. Control returns to the agent on hand-back, when the viewer has been gone 60 s, or when the box ends the connection and the viewer does not come back within 5 s (043). It is kept in plugin storage, so a viewer that connects within 60 s of a restart keeps it (045).
- Pausing the agent happens in the plugin. While a person holds
control every computer tool refuses at once, and taking control closes
the box’s computer-use session. The agent takes control back, when asked,
with
computer_take_control(038).
Computer use
- The driver is Cua Driver, one release pinned with its SHA-256 in each
image, with telemetry and update checks off.
cua-driver serveruns in the desktop session: a user service on Linux, a LaunchAgent on macOS, an elevated task at logon on Windows. - The session. One per box runs
cua-driver mcpthrough the command transport’sspawn, and the plugin speaks MCP over it. It opens on first use and closes after 10 idle minutes, when a person takes control, and when the box is released. - The tools are bb-box’s own, never Cua’s:
computer_windows,computer_observe,computer_screenshot,computer_act,computer_type,computer_launchandcomputer_take_control(see the tools reference). - Trees are text, with references such as
e26that belong to the plugin, within a budget of 300 lines. A reference is found again by its role, name and ancestors, follows its element through a rename, and an action on an element renamed out of sight is refused once (022, 023, 024). - Pixels count only against the latest image of that window, at most 1568 pixels on the long edge, and each image states its scale.
- After each action the plugin reads the tree again and says what changed: windows that opened or closed, and menus and popups with their items.
- Delivery is in the background first, then the foreground when Cua refuses, which the result says. On Wayland, keys go only to the focused window, so in the foreground, and an element Cua cannot click is clicked at the centre of its frame (048).
Files in the app
The Files panel reads and writes through panel-only RPC and the box’s command transport. Its workspace state stays in app memory, keyed by thread and box, across panel unmounts. It restores before the first draw and refreshes directory names quietly. Drafts keep their original open hashes, so a later save still checks the file on disk.
The hidden workspace cache holds at most eight snapshots and 32 MiB of encoded data, evicting the least recently used snapshots. It never writes file contents to browser storage. The app’s lifetime overlay checks retained boxes on change signals and reconnection, and drops a released box’s state across threads. An app reload clears all snapshots. See 078.
Images
049. Each kind has a standard image per backend, built by the setup scripts and rebuilt each night. A project can add a script on top.
- The contract. Every image has the box user with sudo, bb-box’s key
where it is reached over SSH, bb-box’s boot units, BB’s host app, Docker
on first use, the desktop and Cua for desktop kinds, nothing of one
machine,
bb-box-feature, and its record. The kind’s check (bb-box-check-headless,bb-box-check-desktop) checks it. - The record,
/usr/share/bb-box/image.json(/usr/local/shareon macOS,C:\bb-box\image.jsonon Windows), gives the build’s ID and date, the contract, the apt snapshot, the pins’ digest, BB’s app’s digest, the tools, and for a project’s image its script’s digest (src/backends/images.ts). A copy is readable without starting the image: in a template’s description, a Docker label, or a file beside a Tart VM. - Freshness. The doctor fails an image with an older contract, or a GPU image whose NVIDIA userspace is not the host’s, and warns when one is older than twice its interval, or has another BB app or other pins.
- Nightly rebuilds (
src/plugin/images.ts).bb box images refresh, run each night by a BB script automation, rebuilds due standard images one at a time, most needed first, with the latest agent CLIs, gh and Chrome that publish checksums (src/backends/versions.ts). Linux images are due after 7 days and macOS images after 30. A run starts no image after 4 hours. A trial box of the new build must pass the kind’s check, have BB’s app and the record, answer no more than 5 s slower and have no older tool before the build takes the image’s name. Windows images are not refreshed yet (#450). - A project’s image (
src/core/project-images.ts) is a script run as root on a kind’s standard image, with an optional check run as the box user, kept in the configuration with the paths it came from in the project’s repository, usually.bb/box-setup.shand.bb/box-check.sh. The script getsBB_BOX_IMAGE,BB_BOX_KIND,BB_BOX_BACKENDandBB_BOX_ARCH, and none of the project’s secrets. - At start, then baked (
src/plugin/bakes.ts). A box with no current build of its image runs the script as it starts. Once two of its boxes did so on a backend within 14 days, or at once withbake: always, bb-box builds the image there: a box of the standard build, the script and check, finalizing, a capture and a trial box. A changed script or a rebuilt standard image makes a build out of date. The nightly run rebuilds builds in use and removes those unused for 30 days. Proxmox LXC, Docker and Tart bake, Tart on the Mac of the boxes (#439); the SSH host pool and GPU desktops run the script at each start. - Choosing. A project’s threads get its image for the kinds it covers.
box_acquire’simage,bb box acquire --imageand New box can choose another project’s image orstandard. - Written by an agent. An agent drafts the script with
box_image_draftfrom the project’s own files (src/core/draft.ts), tries it withbox_image_try, and registers it withbox_image_set.
Relays through BB machines
A BB machine runs bb-box’s host entry (src/host/entry.ts) in a worker of
BB’s daemon, called through BB. A call takes about 0.4 s, so calls set
things up and carry no commands
(BB request 12).
Bytes go over a WebSocket that the worker opens out to one of bb-box’s
routes with a one-time secret, carrying channels with credit-based flow
control (src/relay/mux.ts).
- access keeps the thread’s variables at hand for commands in a box (see Enrolment and the thread’s access).
- The Docker relay reaches a BB machine’s Docker without SSH
(042). The docker
CLI on BB’s server uses a unix socket that bb-box listens on, and each
connection to it is a channel on the
/docker-relayroute’s WebSocket. It opens with the firstdockercommand, in about 0.5 s, and closes after 5 idle minutes when bb-box has no box on the machine. - The msb relay runs bb-box’s msb on a BB machine
(053).
msb has no daemon or API socket to forward, so each run of it is a
channel on the
/msb-relayroute’s WebSocket, opened and kept as the Docker relay is. bb-box’s msb client (src/relay/msb-client.mjs), run by node on BB’s server, stands in for the msb CLI: it connects to the machine’s unix socket, sends the run’s arguments, and carries its stdin, stdout, stderr and exit status, and for a terminal its size and resizes, as the frames ofsrc/relay/msb.ts. On the machine, the host entry runs onlybin/msbin bb-box’s msb folder, on a PTY of its own for a terminal, and three scripts of bb-box’s (src/backends/microsandbox/release.ts) through itsmicrosandboxcall: install the pinned msb after checking its SHA-256, report on the machine, and import an image from Docker there. The folder ismicrosandboxin the plugin’s data folder on the machine, with msb’s state and images in itshome. msb’s sockets need a home of at most 57 bytes, so a longer one is reached through a link of the user’s,/tmp/bb-box-msb-<uid>. Through a link, such as that one or a path in a Mac’s/tmp, which is itself a link, msb indexes each snapshot under both paths and refuses its ID, so bb-box names a snapshot by its group and member (bbbox-<box>:<name>), which works with or without a link. - File streams carry a
box_copylarger than BB’s file API takes, on the/file-relayroute, from the worker on the machine that holds the thread’s workspace (#492).
Configuration
bb-box’s configuration is one checked document in its plugin storage (029), described field by field in the configuration reference.
- It lists the backends in the order the registry tries them, each with its fields, switch, warm counts and hosts; the kinds, each with its switch, presets, default, largest size and spares; the general settings; and projects’ images.
- Hosts set up alike are a list inside one backend. Docker has a backend per Docker host, as each BB machine with Docker is one; two on one host are refused. Other types have one backend each.
- Switches stop new boxes only: an off backend’s boxes keep working.
- A backend reads its switches, warm counts and kinds’ sizes through its context, as they are at the call, so changing them does not build it again. A driver may take a set of fields from another type’s backend: Proxmox QEMU takes Proxmox LXC’s connection and SSH key.
- Secrets are kept apart from the document, and are write-only.
- The settings page and
bb box config show | check | applyuse the same RPC. Every write is checked, and a revision stops two people overwriting each other.
Data ownership
| Data | Lives in | Owned by | Survives |
|---|---|---|---|
| Box records: ID, thread, kind, backend, guest, state, timings | plugin storage | core | restored by the sweep from the guests if lost, while their threads are live |
| Which guest belongs to which box and thread | the provider, or a marker on a real machine | backend | the plugin’s storage being lost |
| Warm pool membership | the backend’s pool store | backend | as above, for tags; plugin storage otherwise |
| The configuration | plugin storage | the operator | exported by bb box config show --json; deleted by bb plugin remove |
| Provider credentials, the files for every box | plugin storage, apart from the configuration | the operator | never in output, logs or errors, nor sent to the browser |
| bb-box’s SSH key | plugin storage; setup installs its public key | core | |
| The install ID | plugin storage, and on every guest | core | the doctor shows the old ID, and an operator command restores it |
| The thread’s Git credentials and variables | BB | BB | read from BB’s daemon in the box by each command, never written to disk |
| Work in a box | the box’s disk | the thread | deleted on release, or the home recreated on a real machine; not backed up |
| Standard images and builds of projects’ images | the provider | the operator, and bb-box for rebuilds | rebuilt each night, or by the setup scripts |
| Who holds each desktop’s input | plugin memory, and plugin storage while a person holds it | core | after a restart, the agent’s unless the holder’s viewer connects within 60 s |
| The box log: each box’s starts, stops, resizes and releases, who asked, and what bb-box saw after a failure (069) | box-log.db beside the plugin’s data.db, attached to its handle, capped at 8 MB; inside data.db under a row cap where the handle refuses to attach it |
core | a box’s last 7 days and its latest event of each kind; a released or forgotten box’s for 24 hours |
| Box history: each running box’s light readings, a point a minute and a rollup an hour, its counters between readings, when it was off, and each hour’s tallies of readings due, taken, skipped and failed (069) | box-history.db, attached the same way, capped at 64 MB; inside data.db under 200,000 minute points where the handle refuses to attach it |
the plugin’s reader (src/monitor/box-history.ts) |
minute points 24 hours, hourly points and off times 7 days; a released or forgotten box’s 24 hours after it; a full file stops history’s writes alone, until pruning makes room |
Backends
What runs where:
| Host | Box | Form | Backend | Kinds |
|---|---|---|---|---|
| Linux | Linux | container | Proxmox LXC | linux, linux-desktop, linux-desktop-cpu, linux-desktop-wayland-cpu |
| Linux, Windows (WSL 2, Docker Desktop) | Linux | container | Docker, local, over ssh:// or a BB machine’s |
linux, linux-desktop, linux-desktop-cpu, linux-desktop-wayland-cpu |
| Linux | Linux | microVM | microsandbox, local, over ssh:// or a BB machine’s |
linux, linux-desktop-cpu |
| Linux | Linux | physical | SSH host pool | linux |
| Linux | Linux | VM | Proxmox QEMU | linux |
| Linux | Windows | VM | Proxmox QEMU | windows-desktop |
| Mac | macOS | VM | Tart | macos, macos-desktop |
| Mac | macOS | physical | SSH host pool (047) | macos, macos-desktop |
| Mac | Linux | VM | Tart | linux-arm64, linux-arm64-desktop-cpu |
| Mac | Windows | VM | Tart, in QEMU | windows-arm64-desktop |
| Hetzner Cloud, servers it makes | Linux | container | Hetzner Cloud | linux, linux-desktop-cpu, linux-desktop-wayland-cpu |
| Namespace, Macs it rents by the minute | macOS | VM | Namespace | macos, macos-desktop |
| Namespace, Linux machines it rents by the minute | Linux | container | Namespace | linux, linux-desktop-cpu, linux-desktop-wayland-cpu |
| Proxmox LXC | Proxmox QEMU | Docker | microsandbox | SSH host pool | Tart | Hetzner Cloud | Namespace | |
|---|---|---|---|---|---|---|---|---|
| Nature | virtual | virtual | virtual | virtual | real | virtual | virtual | virtual |
| Acquire | linked clone, start | resume a hibernated warm VM, or a linked clone | create and start a container | msb create |
claim a machine | tart clone, tart run; QEMU for Windows |
a container on a host with room, or on a new host made from the host image | create an instance with a deadline an hour away, wait for SSH; for Linux, pull the image and make a container |
| Release | delete | delete | remove the container | msb remove |
reset the machine | tart delete |
remove the container; a host idle for 30 minutes is deleted | leave the tailnet, destroy the instance |
| Commands | SSH | SSH; OpenSSH with PowerShell 7 on Windows | docker exec |
msb exec |
SSH as the box user | SSH through the Mac | docker exec on the host, over SSH |
SSH through Namespace’s gateway; docker exec over it for Linux |
| Ownership | tags and description | tags and description | labels set at creation | labels set at creation | a marker on the machine | an owner file per VM on the Mac | labels on servers and containers, and host records | labels set at creation, and a unique tag per box |
| Capacity | memory, disk, VMIDs, GPU | memory, disk, VMIDs | memory, a container budget, desktops per GPU | memory, cores, a microVM budget | machines | macOS VM slots, memory, disk | each host’s memory, most hosts, a monthly spend limit | most machines, the workspace’s Mac cores and memory, its largest Mac |
| Stop and start | yes | yes, by hibernating | yes | yes | no | yes, by suspending or saving | yes | no |
| Warm guests | started clones | hibernated clones; Linux has spares only | none: a box is ready in about a second | none: a box is ready in under half a second | free machines | suspended macOS VMs, saved Windows VMs | none: a host more than three quarters full makes the next ahead | none: a Mac is ready in about 20 s |
Proxmox LXC
Code in src/backends/proxmox-lxc/, with what Proxmox backends share in src/backends/proxmox/: the API client, ownership tags, TLS pin and settings. Set up by setup/proxmox-lxc.
- A box is an unprivileged container, a linked clone of a CT template
(
linux, a GPU or CPU desktop, or the Wayland desktop), withnesting=1for Docker and/dev/net/tun. Root builds the templates; the API token only clones, configures, starts, stops and deletes in its pool. - An acquire clones, sets the address, cores and memory, writes the
description that names the install and box, then the tags (
bb-box,install.<id>,box.<id>,kind.<kind>and the rest), starts the box, grows its disk if asked, and waits for SSH. Boxes get static addresses from a range, round-robin, with no DHCP. - A template’s build ID is a tag that clones copy. A warm box from another
build is not handed out, and
maintain()replaces it. A clone that finds its template away whilebuild.sh --replaceswaps it waits up to 10 s. - A GPU desktop that started on the CPU path is released and tried on
another GPU’s template. On the GPU path the backend resizes the screen
with
xrandr --fb, whichx0vncservercannot. - Each change to a guest tries again for up to 2 minutes while another Proxmox task holds the guest’s lock.
- A warm box serves only a box with its cores, as a running container takes new cores only when Proxmox next rebalances CPUs.
Proxmox QEMU
Code in src/backends/proxmox-qemu/, on the same Proxmox client. Set up by setup/proxmox-qemu, on a host set up for Proxmox LXC.
- A box is a linked clone of a Windows template that is not generalized
(039).
Its address reaches Windows in the SMBIOS serial, which
boot.ps1reads at each boot. A larger disk grows before the VM starts, and the acquire extends C: over it. - Commands run over OpenSSH in PowerShell 7, elevated, as the template’s
administrator
box. The QEMU guest agent is for readiness only. - The desktop is the VM’s console through the API’s VNC proxy. The VirtIO GPU driver ignores a viewer’s size, so the backend sets the mode through a desktop command, no smaller than 800x600.
- Cua Driver runs elevated in the desktop session, on the pipe
\\.\pipe\cua-driver. - A stop hibernates the VM to a state volume, and a start resumes it with
its apps open, then sets Windows’ clock. Warm VMs are hibernated clones,
tagged
warm, as Proxmox refuses a hibernated VM’s config changes; a claim resumes one, then gives it the box’s tags. - A
linuxbox is a linked clone of a Linux template thatbuild.sh linuxmakes from Ubuntu 24.04’s cloud image with Proxmox LXC’s guest scripts (057). Itsbb-box-bootunit reads the address from the SMBIOS serial before the network starts, and grows the root over a larger disk. Commands run in sh asbox, and it joins BB as other Linux boxes do. It has no warm pool: its spares wait hibernated, as Windows’ do. A stop hibernates it.
Docker
Code in src/backends/docker/. Set up by setup/docker, and on Windows by setup/docker-wsl or Docker Desktop.
- bb-box runs the
dockerCLI on BB’s server, against a local daemon, one overssh://, or a BB machine’s through the Docker relay. It reaches a box only throughdocker exec, so BB’s server needs no network path to a box (031). - A box is a container named
bbbox-<box ID>, with labels set at creation that name the backend, install, box, thread, name and kind. Its size is--cpusand--memory; it shares the host’s disk, so a disk size is refused. It runs under--initwith the image’s start script and no systemd (013), with/runas a tmpfs and the restart policyunless-stopped. - A box is privileged, with Docker’s data on an anonymous volume, unless
privilegedis off (041). Its start script listens on Docker’s socket, sodockerdstarts with the firstdockercommand, then writes/run/bb-box/ready, which bb-box waits for before it runs BB’s bootstrap. - A GPU desktop gets one GPU through CDI. As it is privileged, it sees every GPU, so its start script points Xorg at the one placed, by its PCI bus ID (#283).
- Images are built on the Docker host by
setup/docker/build.sh, from the Proxmox LXC template’s guest scripts; bb-box never pulls one. On a BB machine, bb-box builds what is missing or out of date itself.
microsandbox
Code in src/backends/microsandbox/. Set up by setup/microsandbox (053). It is an option beside Docker, for boxes that need a kernel of their own, never the default: it stays last in the backend order, so a box gets a microVM only when it asks for this backend.
- bb-box runs its own
msbon one host: Linux with KVM, or a Mac with Apple silicon on the Hypervisor framework; BB’s server, one overssh://, or a BB machine through the msb relay. It installs the release it pins, 0.7.7, checked by its SHA-256, in a folder of its own there, withmsb’s state and images, the first time it needs it. - A host offers the kinds of its architecture, from its facts, kept in
storage for the next start:
linuxandlinux-desktop-cpuon x86_64,linux-arm64andlinux-arm64-desktop-cpu(040) on arm64, as a Mac. - On a host without Docker to import an image from, as a Mac, bb-box builds
it there in a builder microVM,
bb-box-builder-<id>, fromdocker:dindpinned by digest, withsetup/docker/build.sh --archfor the host’s architecture, moves it intomsb(docker savein the builder,msb loadon the host), and removes the builder. One build runs at a time; a box of its kind is refused asno_capacityuntil it is done, so the registry tries the next offer, such as Tart. - On such a host the nightly refresh (049) rebuilds the images too: the
backend’s
buildmakes a new one in a builder with--bb-url, under<image>:refresh-<time>; a trial box of it,bbbox-image-<id>, runs Docker’s check; andreplaceputs it under the image’s name withmsb saveintomsb load, asmsbhas no tag command, which share their layers. The images’roomasks for 80 GiB free on the volumemsbkeeps them on, measured through any link, so a short host’s image waits rather than fails. On a host with Docker it lists no builds:msbimports Docker’s. - A box is a microVM named
bbbox-<box ID>, with labels set at creation that name the backend, install, box, thread, name and kind;msb modifychanges the thread and name when the box changes hands. Its size is--cpus,--memoryand--root-disk, a sparse disk of 32 GiB unless a size is asked for./runis a tmpfs, and its network profiles arepublic,private, which reach BB’s server on a private or tailnet address. - With Docker on, as by default,
/var/lib/dockeris an ext4 disk of the box’s own, as overlayfs cannot sit onmsb’s root:msb’s own (--mount-owned), whichmsbcaptures in each snapshot and gives each box restored from one as a copy of its own (070). A box made by an earlier bb-box has a file of the host’s instead, in the folder’sdisks, passed with--mount-disk;msbleaves such a disk out of a snapshot and cannot give it to a restored box, so such a box takes no snapshot. The file goes with its box, and the upkeep removes one no microVM has. The image’s start script startsdockerdwith the firstdockercommand (041). It needs no privilege on the host: each box has its own kernel. msb’s agent runs the image’s start script at the first boot only, sostartruns it again throughmsb execaftermsb start, then waits for/run/bb-box/ready.- Snapshots (070) are
msb snapshot create, with--fullfor memory, in a group per box,bbbox-<box ID>, named<install>-<snapshot ID>-r<identity reset version>so the listing alone says whose each is.msbcannot change a snapshot’s labels once taken, so a keep date is a file in the folder’skept. A snapshot outlives its microVM. A restore ismsb snapshot restoreinto a new microVM,bbbox-<box ID>-<token>, which keeps none of the old one’s settings: bb-box sets the box’s labels and environment on it withmsb modify, and each start mounts/runin memory again and sets the box’s hostname. A fork restores the snapshot’s disk only, never its memory, under the fork’s own name and identity value. Each snapshot’s parent is the microVM’s previous capture;msbrefuses to delete a parent without--force, though a child holds its own data, so bb-box passes it, and moves a group’s head to another member before deleting the head. bb-box does all this only with themsbrelease whose snapshots it measured. - Commands, copies and the desktop’s stream go through
msb exec --stream, which gives output as it comes.msb execof a stopped box boots it for the command, so the transport’s probe asksmsb pingfirst. As over SSH, what a command leaves running survives itsmsb execonly withnohupand its output redirected. - Images come from Docker on the host, built by
setup/docker/build.sh: bb-box imports a kind’s image withdocker saveandmsb loadat its first box, and again when Docker’s image ID changes; bb-box never pulls one. - Capacity is the host’s memory, less a reserve and the memory of the
running bb-box microVMs, its cores, and a most-boxes budget counting every
install’s.
msbreserves no memory and takes back what a guest frees. There is no warm pool yet. - A guest keeps its page cache in its own RAM, which the host cannot share
or take back, and enrolment fills it. At each boot bb-box sets the guest’s
page_reporting_orderto 0, so it reports free memory tomsbin pages rather than 2 MiB blocks, and each upkeep, every 3 minutes, has each running box whose load is under half its cores and with over 256 MiB cached drop its caches and compact its memory. An enrolled idle box then held 0.5 to 0.65 GiB of the host, where it held 1.6 to 1.9 GiB (053).
SSH host pool
Code in src/backends/ssh-pool/. Set up by
setup/ssh-pool, which installs the helper,
machine.sh or machine-mac.sh, the only thing bb-box runs as root.
- bb-box logs in as an admin user whose key is
restricted and whose sudo rule allows only the helper. The helper reads and printskey=valuelines, and holds a lock on the machine while it acts. - A box is a new user on the machine,
bbx-<generation>. Ownership lives on the machine, in/var/lib/bb-box(/var/db/bb-boxon a Mac): the reset generation and anownermarker, written before the user is made. A release renames it toresetting, so a cut-short reset is finished. - A reset removes the user and everything it owns. With
reset=machine, it runs the operator’s whole-machine reset first, which is what gives the pool’s boxes Docker, when every machine has one. - Capacity is one
machineslimit. A claim takes the smallest free machine of the size, then one without a whole-machine reset, then the one free longest. - Macs (047)
lend
macosandmacos-desktop. The next box’s user is made and signed in at the console before the claim, through the automatic login, which needs FileVault off; on macOS 27 the reset restarts user space for it. The desktop is Screen Sharing with the viewer’s input throughMacInput. A Mac with Xcode adds the featuresxcodeandios-simulator. A Mac without FileVault off, Full Disk Access or its box user at the console is out of service.
Tart
Code in src/backends/tart/: settings in
config.ts, the scripts it runs on a Mac in host.ts, Windows in
qemu.ts, and the desktop holder in vnc-holder.ts. Set up by
setup/tart.
- bb-box reaches each Mac over SSH as a host user, with its host key pinned, and each VM through the Mac as the jump host. VMs sit on the Mac’s own NAT.
- A box’s VM is
bb-box-<box ID>. Its owner file,~/.bb-box/tart/owners/<VM>, is written with noclobber before the clone and removed after the delete.tart runruns detached, with its exit status in~/.bb-box/tart/run/<VM>.exit, whichstopped()reads with macOS’s crash report. - Capacity per Mac: at most two macOS VMs, counting every VM Tart runs there (Apple’s licence and the Virtualization framework’s limit; Linux VMs do not count); memory less a reserve; and disk. A macOS or Linux box keeps its image’s disk.
- macOS boxes stop by suspending. Warm VMs are suspended clones, which take no slot and no memory until claimed. A warm VM serves while its image’s build, the default size, Tart’s version and the Mac’s macOS build are the ones it was filled with. The desktop is Tart’s VNC server, which crashes the VM on a second connection in a run, so a holder on the Mac makes the run’s only connection and serves every viewer (#406).
- Linux boxes (040) are clones of a second image, built from the Proxmox template’s guest scripts for arm64. Tart cannot resume a Linux VM, so a stop shuts it down, and there are no warm Linux VMs.
- Windows boxes (046) run in QEMU with HVF, as a second engine of the backend, so one survey counts every VM’s memory and disk. A box is a qcow2 overlay on the image’s current build, saved to a file when stopped; warm VMs are saved the same way.
- A project’s image is built on the Mac of its boxes, and its boxes there are clones of that build, never warm VMs (#439).
Hetzner Cloud
Code in src/backends/hetzner/: the API client
in api.ts, host records and the sweep in hosts.ts, how bb-box reaches
a host in reach.ts, the host image in host-image.ts, first-boot data in
cloud-init.ts, and the month’s spend in spend.ts. Set up by
the Hetzner Cloud guide
(055).
- A box is a container on a Docker host that the backend makes in a Hetzner Cloud project that holds nothing else. Each host is a Docker backend of its own, given the CLI that reaches it, so a box there is what it is on any Docker host. The kinds are Docker’s less GPU desktops.
- A box goes on the ready host with the most free memory that fits it, else on a host being made, else on a new host, which the acquire waits for. A host more than three quarters full makes the next one ahead. A host with no box for the idle time, 30 minutes by default, is deleted.
- A new host starts from the host image, a snapshot with Docker, Tailscale and the box images, which the backend rebuilds weekly and when BB’s app or the image scripts change, keeping the 2 newest.
- bb-box reaches each host’s Docker over SSH with a key of its own: on the tailnet, which the host joins at first boot with a one-off key minted for it, or else over the internet from the addresses in SSH from alone. The host’s firewall lets in only Tailscale and ping, and boxes cannot reach the metadata service.
- Records: every server carries
bbbox-install=<install ID>, and each host has a record written before the server is made. The sweep finishes or deletes a host whose making or deleting was cut short, takes back a host whose record was lost, and deletes another server with the install’s label 10 minutes after it was made. - Limits, each refused with its reason so the next backend can take the box: most hosts (2), a box bigger than the server type holds, and a monthly limit on spend before VAT, counting the month’s hosts by started hour up to the monthly price, and the next hour of every host.
Namespace
Code in src/backends/namespace/: the Compute
API client in api.ts, settings in config.ts, the backend in
backend.ts, Linux boxes in linux.ts and their images in
linux-images.ts. Set up by the Namespace guide.
- A box is a Mac of its own, an instance Namespace rents by the minute: the smallest of Namespace’s fixed shapes with at least the box’s cores, memory and disk. An acquire creates it with labels for the install, box, thread, name and kind, a unique tag from the install and box, so a create tried again finds the first, and a deadline an hour away.
- Commands run over SSH through Namespace’s gateway, with bb-box’s own
key, as
runner, an administrator. Amacos-desktopbox is made ready as it starts:bb-box-streamis built with the Mac’s Xcode; Screen Recording, Accessibility and event posting are granted in the system TCC database, as SIP is off; the screen-capture notice is put off; the display is kept awake; and Cua Driver is installed for computer use. The desktop is Screen Sharing through an SSH tunnel, signed in with the password Namespace’s API gives, and video frombb-box-stream. - With the tailnet on, the Mac joins it as
bb-box-ns-<install ID>-<box name>with a one-off key given on its input, and leaves it at release. - Records and sweep:
held()lists the install’s instances by label with their owners, so the sweep releases a Mac whose box has gone.maintain()keeps the deadline of each Mac a box records an hour ahead, up to the plan’s longest life, so a Mac bb-box loses track of ends within the hour, and removes the install’s tailnet devices that no Mac has. - Limits: most machines (2), counted across every install in the workspace; and the workspace’s Mac cores and memory, and its largest Mac, read from Namespace’s usage API before each Mac, or, for a token without that grant, from Largest machine or Namespace’s last refusal.
- Linux boxes (#997) are each a Linux instance of their own with a
container of bb-box’s own image, made by a Docker backend whose CLI
reaches the instance with
docker -H ssh://through the gateway, with bb-box’s key and a shared SSH connection; the gateway forwards no Unix socket. The acquire creates the instance, waits for SSH, joins the tailnet, pulls the kind’s image from the workspace’s registry and tags it, and the Docker backend makes the container, with the instance’s memory less a margin. Images are built in a Linux instance of their own withsetup/docker/build.shand pushed from it, as instances are signed in to the registry already; one is rebuilt, while boxes use it, when the setup scripts or BB’s app change, or weekly. A build instance has the labelbbbox-role=image-build, is not a box’s guest, and ends within the hour. - A Mac ends at the plan’s longest life whatever bb-box does. Its guest
says when, which the panel and
box_statusshow, and the box’s thread is told an hour before.
Planned
- Joining a tailnet
(050):
a box, opted in by kind, image or acquire, joins the operator’s tailnet
as an ephemeral node, with a one-off key from an OAuth client, and leaves
it on release or stop. Built so far: Proxmox LXC templates have
/dev/net/tun, and the cloud machines bb-box rents, Hetzner Cloud’s hosts and Namespace’s Macs, join it through the Tailscale OAuth client in BB Box’s General settings (the tailnet guide): a one-off, ephemeral, pre-approved, tagged key for each machine, and a sweep of the devices it no longer has. - microsandbox, next (053): spares made ahead, from forks only on hosts with reflinks, as a fork on ext4 copies the box’s disk and is slower than a new box; the Wayland desktop; and arm64 boxes on a Mac.
- Clouds (055): a cloud VM that runs Linux works today as a Docker host, through BB, or as a pool machine. Built: the Hetzner Cloud backend, which makes Docker hosts as boxes need them and places boxes on them, and the Namespace backend, a Mac or a Linux machine rented by the minute for each box. Later: AWS (#1007), microsandbox on Namespace’s Linux machines (#1013), and a cloud VM per box for Windows and GPU desktops.
- Desktop streams (056):
Linux X11 desktops add a Selkies display, H.264 over WebSockets, which
the relay carries message by message with the same control; and Tart’s
holder sends each viewer updates only as it asks. Built so far: X11
desktop templates and images run Selkies on
127.0.0.1:5980in the box, which takes about 0.1% of a core while no viewer is connected.
The conformance suite and the doctor
(009)
- The conformance suite (
test/conformance/) runs the same checks against every backend, skipping what a kind does not declare, never by backend name. Writing a backend lists them. - The doctor checks each backend end to end, and core’s own parts,
with a stable ID per check and what to do when one fails. It runs from
the settings page, the panel and
bb box doctor, and never prints a credential. The checks are in the reference. - Each backend’s setup is a guide and the scripts it runs, which the settings page links, the guide first (016).
Open questions
| Question | Where it is answered |
|---|---|
| Whether Cua’s references survive a new proxy session; macOS 26’s window-picker consent; driving the iOS Simulator; Cua’s background input on real Xorg | the experiments in computer use |
| What a stand-in Mac could not show: that a person’s Accessibility step lets the input helper post events with System Integrity Protection on, the reset’s sweep with it on, the console’s size with no display, Cua’s first capture for a new user, and Xcode with the Simulator’s first boot | the live check on a Mac set aside, in 047 |
Whether BB’s plugin WebSocket will give bufferedAmount |
BB request 2 |
| Selkies on a GPU desktop: NVENC and NvFBC in a box, and their GPU memory | step 3 of 056 |
| Whether a Docker host that is an LXC container passes TUN to privileged boxes, and how long a tailnet join takes | the first PRs of 050 |