Distributed aite: browsers that listen for work, a central system that plans and collects #
Draft 1, written 2026-10-15. Design only, nothing built. It follows Alex's description: a signed-in browser running the aite extension listens for work requests, pulls a request down, navigates to the site, runs the test while recording browser state, and reports back with an archive. That needs a downloadable extension and a separate central system with auth, a test repository, a result repository and a communication channel (XMPP, probably).
Labels: Verified means I read it in this repo or ran a command today. From memory means I believe it but did not check. To verify means a spike must answer it before work depends on it. Where this file and docs/design/protocol.md disagree about the local bridge, protocol.md still describes the single-machine runner; this file describes the new layer around it. See also architecture-as-understood.md for what exists today, and distributed-network-and-identity.md for the options, facts and working direction on the channel (Concord or XMPP), identity (ATProto) and the VPN, DNS and TLS questions. Where the two disagree, the network and identity file is newer (2026-10-15).
1. What changes, in one paragraph #
Today a Node runner owns the run: it starts Chrome, talks to the extension over a local websocket, drives trusted clicks and screenshots over a private debugging pipe, and writes results to disk. In the new shape the extension owns the run. It is the client of a central service, receives a job, executes it inside the user's own browser, and uploads an archive. The Node runner stays for CI and headless runs, and both executors share the same parser and engine.
2. Components #
| # | Component | Job | Exists today? |
|---|---|---|---|
| A | Worker extension (MV3, downloadable) | Sign in, enrol the device, hold the XMPP connection, fetch a job over HTTPS, run it, record state, build and upload the archive, show status | Partly: the page-side code (descriptors, actions, repair, outline) exists. The connection, the engine host, auth, recording and upload are new. |
| B | Auth service | Users and devices. Issues a device credential after a person signs in. Revokes devices. | No |
| C | Test repository | Holds versioned test bundles: scripts, lock files, journeys, metadata. Source of truth stays in git. | Scripts and locks are files in git, but there are no real scripts yet (62 fixtures only). |
| D | Dispatcher (planner and scheduler) | Decides which test runs on which device and when, creates jobs, tracks leases, retries, fan-out, budgets and caps | No. aite schedule runs a fixed list on one machine. |
| E | Communication channel | Wakes a device with a job, carries progress and status | No |
| F | Result repository | Stores archives and indexes run.json fields for search, trend and diagnosis |
No. Results are local folders. |
| G | Docs and planning layer | Generates docs from results, proposes new and repaired tests, files issues | Pieces: aite author, the report, the verdict service, scheduled issue filing |
A and the five central parts B to F are the new product. G consumes what they store.
3. How the pieces talk #
Rule: XMPP is the doorbell, HTTPS carries the goods, and the database is the truth. A job never exists only as an XMPP message.
person --signs in--> Auth (B) --device credential--> Extension (A)
| XMPP over WebSocket (signalling only)
Dispatcher (D) --job offer {jobId, bundleHash}-------->|<--- accept / progress / done ---
| |
| HTTPS (authenticated with the device credential)
+-- GET /jobs/{id} the job and its bundle (C) ----> extension
+-- PUT /jobs/{id}/archive the archive ----------------> Result repository (F)
- Why XMPP is only signalling. Chrome's service worker cannot open a raw TCP socket, so XMPP must run over WebSocket (RFC 7395), which Prosody and ejabberd both support (from memory, verify). Large payloads over XMPP are fragile, so offers carry ids and hashes and bulk data moves over HTTPS.
- Why the database is the truth. On connect the extension also asks
GET /jobs?device=me&state=offered, so a missed offer (browser closed, network drop) is recovered. XMPP offline storage becomes a nice-to-have. - Leases. An offered job has an
offeredAt. The device mustacceptwithin a set time, then send a heartbeat (a presence or IQ) while running. A missed lease puts the job back in the queue with an attempt count. A job is never run twice at once: acceptance is a compare-and-set on the server. - Presence. A device that is online, idle and allowed to take work publishes presence with capability tags (Chrome version, which accounts it holds, allowed origins). The dispatcher matches on tags.
- MV3 keep-alive. The existing design already handles this for the local bridge (ping every 15 s, reconnect with backoff,
chrome.alarmsbackup, see protocol.md). The same rules apply to the XMPP socket, plus XMPP's own stream management and keepalive. To verify that a signed-in idle browser keeps the connection over hours.
XMPP server choice (Prosody or ejabberd or another) is open, see section 9.
4. Auth and enrolment #
Two credentials, because a person and a device are different things.
- A person signs in to the central system (the identity provider is open, section 9) from the extension popup or a web page.
- The device is enrolled. The central system issues a device credential bound to that person and a device id, with a role (worker) and a set of allowed origins. The extension stores it in
chrome.storage.local. - The device credential authenticates both channels: HTTPS (bearer token) and the XMPP login (a SASL mechanism that accepts a token, to verify against the chosen server). A device cannot read other devices' jobs.
- Revocation. The person or an admin revokes a device from the central system. The server drops its XMPP session and rejects its token. Tokens are short lived and refreshed, so revocation takes effect within the refresh time.
- No password and no cookie ever leave the browser. The signed-in sessions of the sites under test belong to the browser profile, stay there, and are never read by the extension for upload (see section 7).
5. What a job is #
A job is data, never code. This matters because Manifest V3 forbids remote code: the extension may not download and execute scripts. A .aite file is parsed by the parser that ships inside the extension, so downloading one is allowed.
job {
id, createdAt, deviceId, attempt, state: offered | accepted | running | uploaded | done | failed | expired | cancelled
test: { name, bundle: <hash>, file: "scripts/mallbucks/send.aite" }
bundle: <hash> // scripts + lock files + journeys metadata, immutable once published, signed
start: "https://...", allow: [origins] // must be a subset of the device's allowed origins
budget: { totalMs, postsAllowed, spendsAllowed? }, frozen: true | false
needs: { session: "test-qian", chromeMin: "155" }
}
- Bundle. Built by the central system from a git commit of the test repository: scripts, their
.lock.jsonfiles, shared includes. Identified by a content hash and signed with a key whose public half is pinned in the extension build. The extension refuses an unsigned or altered bundle. - Custom checks (
call). They are TypeScript today and run in a stripped forked process (P7-5). An extension cannot run downloaded TypeScript, so in this shape acallcheck must be built into the extension release and named on an allowlist, which the bundle may only reference by name. Anything else is a failure at parse time. (This narrows what a remote test can do on purpose.) - Frozen by default. A remote run uses the lock files in the bundle and never writes them. Repairs found by the extension are reported as proposals in the archive. Writing a new lock file or script is a commit made elsewhere, by a person or by the authoring flow.
6. Running a job inside the browser #
| Need | Today (Node runner) | In the extension |
|---|---|---|
| Parse and run steps | runner/parser.ts (631 lines) and runner/engine.ts (705 lines): neither imports a Node module, and the engine receives its dependencies (deps.request and others) by injection. Verified. |
The same code, hosted in the service worker or an offscreen document. The host supplies request by calling the page-side code directly instead of through a websocket. |
| Find and act on elements | Content scripts in extension/src/ (descriptors, repair, actions) |
Unchanged |
| Real mouse and keyboard | Runner sends trusted input over the debugging pipe (runner/input.ts, cdp.ts) |
chrome.debugger attaches to a tab and sends the same protocol commands (verified in P8-2: trusted events, but it ignores the allow list, see section 7). The "debugging this browser" bar was not seen in Chrome for Testing; untested on branded Chrome. Without it, only synthetic page events are available, which the plan already marks as weaker. |
| Screenshots | Runner, over the pipe, because the extension lacks the permission (protocol.md) | chrome.debugger Page.captureScreenshot, or captureVisibleTab with the activeTab or <all_urls> permission (the latter gives up the origin allow list). To verify which is acceptable. |
| Network idle, console | Runner (network events), extension events | chrome.debugger network events, extension console events |
| Waiting for downloads, tabs | Mixed | chrome.downloads, chrome.tabs (already used by the extension) |
| Results on disk | results/<run>/ |
Held in memory and in chrome.storage or IndexedDB during the run, then built into one archive |
| Report | report/render.ts (static HTML, no script) |
Rendered in the extension, included in the archive |
| Navigate to the site | navigate command |
Same command: the extension opens a tab on the job's start URL |
The archive is one zip: run.json, report.html, step screenshots and page text, the console and network records, the extension, Chrome and bundle versions, the device id, repair proposals. Uploaded over HTTPS, then the job is marked uploaded. If the upload fails the archive stays in IndexedDB and is retried; a job is done only after the server confirms the hash.
Recording browser state is the same list as today (run.json fields, screenshots, page text for failed steps) plus whatever the planner asks for in the job (for example a network log). Redaction rules are in section 7.
7. Security: the central system can now steer real, signed-in browsers #
This is the main risk of the whole design and deserves the most review.
| Threat | Control |
|---|---|
| A compromised central system sends a job that acts as the user on a site they are signed in to | The device's allowed origins are set at enrolment and enforced inside the extension: a job whose start or any goto leaves them fails. The central system cannot widen them. An origin is also unusable until the person at the device approves it (aiteWorker.setApprovedOrigins; nothing is approved after enrolment, and enrolling again asks again). Approval can only narrow what the build and the enrolment allow; a new origin beyond the build still means a new build. The popup that asks is part of packaging (#62). Built, issue #68. An open build (decided 2026-10-16) gives this up on purpose: it tests any site a signed job names, except the ones its person has put on their list; see the open mode row of security-review.md for what it costs and what still limits it. |
| Tests that post, pay or delete | posts caps per device and per day (carried over from P7-3). A separate capability for any script that spends (spends header, budget.spendsAllowed in the signed job, a device switch that is off by default and after every enrolment, and its own daily caps, 3 per device and 5 for the system; #69). Posting is also off at the device until the person switches it on. A device-side kill switch (popup toggle) and a server-side revoke. |
| A forged or altered test, or an old one kept alive | Bundles signed with an expiry by a release key that is kept off the serving host, key pinned in the build, hash checked before parse; each job signed by a separate dispatch key (also pinned) over the device, attempt and every limit; a signed revocation list with a number that only goes up, which a device must have before it runs anything (built, issue #67; the dispatch key is still on the serving host, see security-review.md) |
| Using the user's everyday profile | Recommended and documented: run a dedicated browser profile for aite. The extension shows the profile it is in (#70): an id it made up for the profile (Chrome gives extensions no profile name), the labels and accounts set on the device, and in every archive the label, account and id; a start page that shows another account, or signed out, ends the run before any step. It cannot tell a dedicated profile from an everyday one, and reading the Chrome sign-in email (chrome.identity) was left out because it would add a permission to every install. |
| Leaking secrets or personal data in archives (screenshots show handles, balances, addresses) | Screenshots and page text are taken only for failed steps unless the job asks otherwise (as today). Archives are private to the owner and the project. Job-level captureScreenshots: false for sensitive tests. No cookie, storage or header capture. Typed values from type commands are masked for password-like fields (the recorder already refuses to record passwords). |
| A page under test attacking the extension | Existing rule: everything from the page is untrusted text, escaped in the report. Keep it. |
| Job replay or cross-device reads | Per-device tokens, job ids bound to a device, server-side compare-and-set |
| A stolen device token | Short lifetime, refresh, revocation, and the token alone cannot widen origins |
| Chrome's own limits on distributing extensions | See section 9. A policy-installed extension can be used by an organisation, but it also means admins can push updates to every device: sign releases and keep the update channel under the same key control. |
D5 in the plan (bridge on 127.0.0.1 only) stays true for the local runner. The new outbound XMPP and HTTPS connections are a separate, authenticated surface, so the plan's security section needs a new entry rather than a change.
8. What carries over and what is new #
Carries over unchanged: the script format, parser, engine, element descriptors, locator repair and its thresholds, lock file format, run.json schema, the report renderer, the verdict flow, the secret scan (applied when a bundle is published: a bundle with secret-looking text is not signed). The verify guard and the checks guard are repository CI guards and are not applied at publication.
Changes: the engine host (extension instead of Node for remote runs), how input and screenshots are obtained, where results are held until upload, how lock files arrive (in the bundle), and how call checks are chosen (built in, by name).
New: the extension's connection, auth, enrolment and job loop; the central service (auth, jobs, bundles, results, dispatcher); the XMPP deployment; archive building and upload; the planner and docs layer.
9. Decisions Alex needs to make #
Defaults are my recommendations. Updates from 2026-10-15: Alex accepts that external devices need a VPN, so D9 is now "behind the VPN" and D12 is limited to VPN-only devices. D10 and D11 are leaning XMPP (Prosody with a custom auth module) with ATProto proving the person; Concord is the alternative, see the companion file.
| # | Decision | Default | Why it matters |
|---|---|---|---|
| D9 | Where the central system runs | A small VM that is not the production droplet, on its own domain; not balrog | The droplet is already crowded and published ports bypass Caddy (aurora-locus-tidi #21). Balrog is a desktop. |
| D10 | XMPP server | Prosody (small, WebSocket support), or ejabberd if scale is expected | Must support RFC 7395 and token-based login. To verify. |
| D11 | Person identity | Reuse an existing login if one fits (this org already runs ATProto OAuth on pds.malldao.xyz, but its OAuth is the thing aite #22 is blocked on today), otherwise a standard OIDC provider |
The sign-in the extension and the dashboard need |
| D12 | How the extension is distributed | Chrome Web Store unlisted for the public path; unpacked or policy-installed for the first devices | Chrome blocks off-store installs for most users on Windows and Mac (from memory, verify); Linux developer mode works |
| D13 | Test repository | Keep git as the source of truth; the central system builds signed bundles from a commit | Reviewable and diffable (plan principle 3), and no second place to edit tests |
| D14 | Archive retention and who can read results | Private per project; retention measured in weeks by default | Archives show real pages |
| D15 | Is the Node runner kept | Yes, for CI and headless, sharing the engine | A virtual passkey and CI need it (D2 already said so) |
10. Phases (tests first, each phase leaves the existing system working) #
Size is relative effort, not a date. S a few days, M a week or two, L several weeks.
| Phase | Work | Size | Done when |
|---|---|---|---|
| D0 | Spikes: engine runs inside an extension offscreen document or service worker on a fixture page; chrome.debugger gives trusted input and screenshots; XMPP over WebSocket from a service worker stays up for an hour; token login to the chosen server |
M | Each question in "To verify" is answered with a note and evidence |
| D1 | Make the engine host-neutral: the engine and report run from a bundle in the browser and in Node against the same tests | M | The fixture command scripts (fixtures/commands/scripts) pass under both hosts, coverage held at 70 percent per level |
| D2 | Central skeleton: job store, bundle build and signing, result upload, dashboards later. HTTPS only, no XMPP yet | M | A job created by hand is fetched, run and archived by a device that polls |
| D3 | Auth and enrolment, device credential, revocation | M | Revoked device cannot fetch or upload; origins enforced |
| D4 | XMPP wake-up, presence, leases, heartbeats | M | A job offered while the browser is open starts within seconds; a missed offer is recovered by polling |
| D5 | Dispatcher: capability matching, retries, caps, kill switches | M | Two devices, one job each, no double run |
| D6 | Planner and docs layer: coverage view from covers tags, repair queue, generated docs, issue filing |
L | Failing run produces one issue with the archive attached |
| D7 | First real tests (mallbucks send, gift, shop) run through the whole path on two devices | M | Evidence in issues P6-1 to P6-3 |
The mallbucks sign-in blockers are not on this list on purpose: they belong to the first test target, not to aite.
11. To verify (answered in D0) #
- Answered 2026-10-15 (P8-1, #52): yes.
parser.ts,engine.tsand the extension's command handler bundle to 74,664 bytes with nonode:import and no replaced dependency, andfixtures/hello.aitepasses inside a real extension, hosted in either the service worker or an extension page, on fresh profiles (3 of 3 each), and a seeded failure fails. Evidence and the driver:docs/spikes/p8-1/README.md. Still open: trusted input (item 2), long-run lifetime of a service worker, andchrome.offscreen. - Answered 2026-10-15 (P8-2, #53):
chrome.debuggergives trusted click and typing (isTrustedtrue) and PNG screenshots from inside the extension. But with thedebuggerpermission the extension also drove a tab outside its allow list, so the allow list must be enforced in our code. The "debugging this browser" bar was not seen in Chrome for Testing 155 (headed, with and without the debugging pipe); branded Chrome and bar dismissal are untested. Evidence:docs/spikes/p8-2/README.md. - Partly answered 2026-10-15 (P8-3, #54): yes for minutes (20 s pings or a 30 s alarm), a stopped worker resumes via stream management or binds fresh, no offer lost in the tests; hours still to confirm. See
docs/spikes/p8-3/README.md. - Which XMPP servers accept token login for a browser client, and how revocation reaches an open session.
- Archive size and the limits of storing it in the extension before upload.
- Chrome distribution rules for the chosen devices.
- Answered 2026-10-15 (P8-2):
captureVisibleTabis refused withoutactiveTabor<all_urls>;chrome.debuggerPage.captureScreenshotworks. Use the debugger path, and enforce the allow list in code (see item 2). - Spikes 8 to 12 in distributed-network-and-identity.md section 7 (Prosody token login, extension WebSocket to a private host, extension fetch to the PDS, VPN on the droplet, the Concord patch if chosen). Spike 10 is partly answered, and it exposed that the PDS cannot resolve its own handles yet (see that file and root/aurora-locus-tidi #22).