Several beam hosts
Administering several beam hosts
One beam host per team or per purpose is a normal shape. An installation still connects to exactly one host, so the fleet is administered from one installation of yours, as each host's operator, with the Beam fleet vApp. It reads the hosts from your machine's server, so the hosts need no extra route and no CORS setup.
Drawing the diagram…
Which token each machine holds
| Machine | Holds |
|---|---|
| Every installation, desktops included | Its own slot's token from BEAM_SLOTS, with the slot typed into Settings > Beam |
| The installation running Beam fleet | The same for its own tunnel, plus each host's BEAM_TOKEN in the vApp's binding |
| Nobody else | A BEAM_TOKEN |
Leaving the slot empty and sharing the operator token works, and every machine finds a slot of its own, but each of them can then open any slot, read the dataset and sync into one shared archive. A slot token opens one slot and syncs into an archive of its own.
Moving a machine from the operator token to its slot token moves its logbook sync too: its next sync uploads its archive again into the new one, and what it synced before stays on the host, readable only with the operator token.
Then choose Fleet sign-in on each installation (Signing in through the fleet). Fleet admins is the safe choice for now: a member's session can still run a shell as that machine's user, and every slot shares one origin. A machine run from a vendor document sets it there instead, as "beam": { "fleetSignIn": "admins" }.
BEAM_OIDC_ADMIN_GROUPS has no default. Unset, the host counts no member as an admin and says so at startup, so Fleet admins lets nobody in. Set it to the group you mean (Fleet sign-in).
Importing the vApp
vApps > Import, then Beam fleet under Templates. The preview names one aperture, beam, left unbound; press Import. The vApp arrives with no hosts, no binding and no token, which is deliberate: it belongs only on an installation that holds operator tokens, so it is never there until you import it.
Adding the hosts
Open the vApp's Backstage, then Rows. Type hosts as the collection and add a row per host:
{ "name": "team-a", "url": "https://beam-team-a.example.com" }name is what the Host picker shows and url is what it publishes, with no trailing slash.
Binding the aperture
Backstage > Apertures > beam. Pick http as the source, leave the target empty, and put this in Preset, one origin per host row:
{
"origins": [
{
"origin": "https://beam-team-a.example.com",
"allow": [
"GET /__beam/health",
"GET /__beam/slots",
"GET /__beam/audit",
"GET /__beam/peers",
"GET /__beam/market",
"GET /__beam/dataset/stats",
{ "route": "POST /__beam/dataset/publish", "confirm": true }
],
"bearer": ""
}
]
}A masked field per origin appears under the preset, labelled with the origin and bearer: paste that host's BEAM_TOKEN into it. Keep "bearer": "" in the preset itself; the field is where the token goes, and a preset entry without bearer gets no field. Leave Capability tag empty and the runner, webhook and peer switches off, and save. From then on the token reads as (secret) everywhere, including to agents, and leaving its field empty keeps it.
The allow list is the point. With it, the tokens can reach the routes the vApp uses and nothing else, whatever a layout or an agent asks for: not the dataset export, not the route that sends a message into an installation, not the marketplace's credentials, and not any slot behind the host.
The publish entry is written with "confirm": true. Publish in the vApp asks as it always did; an agent that calls the route through the aperture asks you on a card first, and a runner or a webhook is refused.
If you keep your hosts in Pulumi, copy each token without it ever appearing on screen:
pulumi stack output beamToken -s <stack> --show-secrets | pbcopyThe tiles
Pick a host in Host. The vApp remembers it the next time you open it.
| Tile | Shows | Runs |
|---|---|---|
| Health | What the host answers anybody: status, live slots, storage, analytics, MLflow | When you pick a host |
| Slots | Every connected slot: machine, since, build, platform, container, sessions, uptime, free memory, open channels | When you pick a host, and every 30 seconds |
| Configured | Every slot a token exists for and whether anything is on it; for one that is offline, the machine last on it, its build, when it left and why | With Slots |
| Market | The marketplace as the selected slot sees it | When you select a slot in Slots |
| Dataset | Each tenant's contributed sessions and bytes | When you pick a host, and after a publish |
| Publish | Publishes the dataset to MLflow, after asking | When you press it and confirm |
| Audit | What happened on the host, newest first: machines connecting, leaving, taking a slot over or being refused, shared logbook membership, and setup invites filed, claimed and revoked | When you pick a host, and every minute |
Each of Health, Slots, Market, Dataset and Audit has a button to refresh it by hand.
A slot that is configured and not live is a machine that is not dialing; its own log says why. A tile that shows an error is only that tile:
| Error | Means |
|---|---|
401 | That origin's token is wrong or missing |
404 on Audit | The host is older than its audit sequence |
501 on Market | The host has no Forgejo, so no marketplace |
501 on Publish | The host has no MLflow configured |
409 on Publish | A publish is already running |
| A refusal naming the route and the origin | The allow list or the origins do not cover it: a host row whose url has no matching origin, or a typo in the list |
| Empty build and uptime columns | That installation is older than those fields |
| A configured slot with no last seen | Nothing has dialed it since the host began keeping track, or the host is older than that |
Adding a machine
The short version, for a host you run with Pulumi:
- Add the slot name to the host's slot list and apply, which issues its token and restarts the host.
- Copy the token into the clipboard, as above but from
beamSlotTokens. - On the machine, Settings > Beam: paste the token, type the slot, Check the host, save. Check the host warns if the token is the operator's.
- Choose its fleet sign-in, then fill in Inbound from other machines: for each other machine, its Peers level and a Fleet preset.
- In Beam fleet, the machine appears in Slots, and Configured shows its slot live.
- For its gateways, loadouts and kits, create a setup invite on a machine that already has them and open it on the new one. Keys stay behind unless you pick carry key; the new machine lists the ones it still needs (Beam).
- For a shared logbook, Invite a machine on a member and Join on the new one, or let an agent do it with the fleet tools (Beam).
Operating the fleet with agent tools
An agent whose kit carries the mechabedo fleet category can do most of the above from a session: fleet_status and fleet_list_machines to see the fleet, fleet_logs to read a machine's server log, fleet_request_key with logbook_share_join to put a machine into the shared logbook, the setup_invite_ tools to set one up, fleet_restart to restart one, and fleet_api_call for anything else that machine's API does. Each change asks on a card in the agent's session, and an invite code never reaches the agent (Fleet operations).
Another machine answers only what its owner granted the asking slot under Settings > Beam > Inbound from other machines, and a restart, key request or preset then waits in that machine's approvals for a person. Nothing is granted by default. An agent refused for a missing grant can ask that machine's owner with fleet_request_grant, which raises one card there and is never answered unattended.
To restart a machine from the fleet, grant the asking slot Restart there and call fleet_restart with that machine's slot as instance, or open the machine through the beam as an admin and use Settings > App data > Restart, which is POST /api/app/relaunch. Either keeps everything and stops every running session, and the page waits for the machine to answer again. Updating a machine's build remotely is not there yet.
Running it unattended
A fleet nobody sits at needs two things on every machine: which operations each other machine may ask for, and what is answered without a person. Both are in the configuration document of a hosted installation, which pins them so nobody changes them by hand:
{
"unattended": { "agents": "operations", "remote": "operations" },
"fleet": {
"peers": { "default": "read", "slots": { "ops": "message" } },
"grants": {
"slots": { "ops": ["status", "logs", "restart", "api"] },
"api": { "ops": ["observe", "operate", "configure"] }
}
}
}Here the machine on slot ops reads status and logs, restarts this one, and calls its API with up to configure, and none of it waits for a person. full on either half also answers permission, approval and settings changes; choose it only for a machine whose agents you trust to change those. On a desktop, MECHABEDO_UNATTENDED=agents=operations,remote=operations pins the policy, and Settings > Permissions > Unattended sets it when nothing pins it, and Settings > Beam > Unattended sets the remote half below the grants, with an override per machine. unattended.remoteSlots, such as { "ops": "on" }, pins those in the document (Settings).
Typed keys, sign-in settings, the policy itself, and wiping or restoring a machine always wait for a person. Each machine lists what it answered under Unattended, and every answer is a line in its server log, which fleet_logs reads.
Next
- Running a beam host - slots, tokens and what the operator routes answer
- Beam - connecting one installation
- vApps - how the vApp itself is wired, if you want to change it