Setting up the dispatcher (the central system) #
The dispatcher is the part of the central system that decides which device runs a test. It does not run on its own: one program, npm run central -- serve, holds the job store, signs the jobs, serves the HTTP API the extensions talk to, runs the dispatcher, and (with --xmpp) runs the doorbell that wakes devices. This page sets that program up and shows how to send a test through it. To set up a device that runs tests, see setup-extension.md.
What you need #
- Node 22 or newer (
node -v), git, and the repository checked out withnpm cirun in it. - For the doorbell: rootless podman. The service starts the doorbell's container only when podman is rootless (a rootful podman older than 5.8.8 is open to a known container escape, GHSA-2cvf-wqm6-wr9g, and the service refuses to use it). The doorbell is optional: without it devices find work by polling every 30 seconds, and you must name the device you want.
- A git repository of tests: any repository with scripts under
scripts/ending in.aite(see script-format.md). The service builds a signed bundle from a commit of it. - Chrome is not needed on the central machine. It only serves and signs.
Start it #
Look before you start, so you never run two (a second one fails on the ports, and a stale one keeps old code):
ps aux | grep "[c]entralCli.ts serve"
ss -tlnp | grep -E ":4790|:5290"
podman ps -a | grep aite-xmpp
Then, from the repository:
npm run central -- serve --tests-repo /path/to/your/tests-repo --xmpp --dev-signing
It runs in the foreground (use a terminal multiplexer or a service manager of your choice; this repository installs none). It prints doorbell: connected when the doorbell is up. Stop it with Ctrl-C.
| Flag | Meaning |
|---|---|
--port 4790 |
The API port. |
--xmpp and --xmpp-port 5290 |
Run the doorbell (Prosody 13, pinned by digest, in a rootless container named aite-xmpp) on this port. |
--tests-repo PATH |
The git repository of tests. A job can then name a --ref (a branch or commit) and the service builds and signs a bundle of it. |
--dir DIR |
The data folder. Default ~/.aite/central. |
--dev-signing |
Keep the release private key in the data folder so the service can sign bundles itself. For one machine and for testing only; see "Keys" below. |
--host |
The address to listen on. Default 127.0.0.1, which is what you want. Do not change it to reach the service from another machine: the extension refuses to send its tokens over plain http to anything but its own machine. Use an SSH tunnel (below). |
The data folder #
Everything the service keeps is in one folder (default ~/.aite/central, mode 700):
| File | What |
|---|---|
central.db |
The job store (SQLite): devices, jobs, bundles, events. |
archives/ |
One zip per finished job. |
admin-token |
The token the operator commands use (mode 600). Never print it or paste it anywhere. |
dispatch-key.pkcs8, dispatch-key.pub |
The key that signs each job, as it is offered. |
signing-key.pkcs8, signing-key.pub |
The release key, only with --dev-signing: it signs bundles and the revocation list. |
xmpp-secret, xmpp-central-password |
Secrets for the doorbell (mode 600). |
prosody/ |
The doorbell's own files. |
Back up the whole folder. Keep the .pkcs8 files and the three secret files private. The .pub files are public.
Keys #
Two keys matter, and an extension is built with both public halves pinned inside it:
npm run central -- key
prints the release key (it signs the bundles of tests and the list of revoked bundles) and the dispatch key (it signs every job). They are public; it is fine to paste them into a build command.
- With
--dev-signingthe release private key sits in the data folder, so anyone who can break into the central machine can sign any test. That is acceptable for testing on one machine you control. - For anything shared, make the release key on a machine you trust and keep it off the host that serves:
npm run release -- keygen --out DIRmakes it (it prints only the public key), you put the public key in the data folder asrelease-key.pub, sign bundles withnpm run release, and bring them in withnpm run central -- bundle import --in FILE(anddenylist importfor a revocation list). The service checks each against the public key before it keeps it.
Pair a device #
A device enrols with a one-time code that you make:
npm run central -- enrol --person NAME --any-site --name laptop --code-out /path/to/code.txt
The code is written to the file (mode 600), never to the screen. It works once and lasts an hour (--ttl-min changes that, up to a day). Read it from the file and type it into the extension's popup (setup-extension.md).
--any-siteenrols the device for any site, for an open extension build.--origin https://SITE(repeat for more) enrols it for named sites instead, for a build with a list of sites.npm run central -- device listshows every device, whether it is online and what it reports (Chrome version, the sites it allows, the sites its person does not allow, posting and spending switches).npm run central -- device revoke IDends a device at once: its tokens stop working, its doorbell session is closed and its jobs are cancelled.
Send a test #
Let the dispatcher pick the device:
npm run central -- dispatch --ref main --file scripts/send.aite \
--start https://SITE/page --allow https://SITE --name "what this run is"
or make a job for one named device:
npm run central -- job create --device DEVICE_ID --ref main --file scripts/send.aite \
--start https://SITE/page --allow https://SITE
--allow names the sites the job may touch (the job's own signed list); --start is the page it opens and must be on that list. Other options: --session NAME (the browser profile the test signs in as), --chrome-min N, --fanout all (one job per matching device), --device ID, --total-ms N (the time budget, default 60000, at most 3600000), --posts N and --spends N (allowances for tests that post or spend; see below), --no-screenshots.
Then read the result:
npm run central -- job list [--state done] [--device ID]
npm run central -- job show JOB_ID # the job and its event trail
npm run central -- job archive JOB_ID --out /path/to/result.zip
The archive holds run.json (every step and command, with timings), report.html, the screenshots, device.json (the browser, operating system and clock of the machine that ran it), and archive.json. The event trail also records the address the service saw the device connect from; behind an SSH tunnel that is the central machine's own loopback address.
To halt everything: npm run central -- stop --reason "why" (running jobs are ended, nothing more is offered), and resume to lift it. status shows devices, caps and job counts.
How the dispatcher chooses #
A device is chosen only if it is not revoked; enrolled for the sites the test names; online (from the doorbell); not paused; has posting switched on if the test posts and spending switched on if it spends; its person has not put the site on their list of sites they do not allow; and it has a new enough Chrome and the profile the test needs. Among those it takes the idle one with the fewest jobs, then the one that has waited longest. Tests that post or spend are also limited per device and per day. A request that no device can take is answered with the reason for each device, which is the first place to look when a test goes nowhere. The full rules are in dispatcher.md.
Reach it from another machine #
The service listens on this machine only. To use it from a device elsewhere, give that device an SSH tunnel to the two ports, so that on the device 127.0.0.1:4790 and 127.0.0.1:5290 are the central system. The walkthrough is runbook-laptop.md.
Restart and update #
Stopping and starting the service loses nothing: the data folder holds the state, and the keys persist, so extensions already built keep working. After a code change, stop the old process, check the ports are free, and start again. Running the functional test suite stops the doorbell container, so restart the service afterwards.
When something is wrong #
| What you see | Look at |
|---|---|
port 4790 is already in use (or 5290) |
Another instance is running. ps and ss as above. |
| The doorbell does not start | podman info must say rootless is true. Check podman ps -a for a stale aite-xmpp container. |
dispatch answers 409 "no device matches" |
Read considered: each device says why (offline, paused, switch off, site denied, Chrome too old, no such profile). |
A job stays offered |
The device is offline or paused. A device without the doorbell finds work within 30 seconds. |
| A job fails at once with a reason | The reason names the cause: origin-not-approved, site-denied, a bundle that was revoked or expired, or a script the device refused. |
device list shows a device offline that you expect online |
Its tunnel or its browser is down; see setup-extension.md. |
What the service protects and what it does not is in security-review.md.