# Security QA with Alex (P8-12, #63): the kill switches and origin approval Written 2026-10-15. This is the manual part of the security review: two things only a person at the device can judge. **It has not been held yet.** Everything below was prepared so it takes about twenty minutes and needs no knowledge of the code. The pass bar is written down before anyone runs it and must not be moved afterwards. If a step turns out to be unfair, say so in the results and run it again; do not edit the bar. ## What you need - The central system running on this machine: `npm run central -- serve --tests-repo PATH --xmpp` (loopback only; it starts the XMPP server in a rootless container). - A Chrome profile **dedicated to aite** (not your everyday one) with the worker extension loaded unpacked, built with `--central` and both public keys from `npm run central -- key` (release key and dispatch key). A test page the extension may touch (for example the fixture server on 127.0.0.1:4780). - A test that takes a while (the fixtures have `sleep 25s`), so you can watch it run. **Where the controls are.** In the installed extension (`npm run build:worker`, docs/runbook-laptop.md) click the extension's icon: the popup has the pause box, the site boxes, the posting and spending boxes, the posts a day and the accounts. The steps below name the calls (`aiteWorker.setPaused(...)` and so on); each is the same as the popup's control, so use whichever you have. Part F is about the popup and the install itself. ## A. The kill switches (pass bar: each ends the run within 20 seconds, and you can tell, without reading code, that it did) 1. Enrol the device with a code you make (`npm run central -- enrol ...`). Look at the extension's state: can you tell that it is enrolled, as whom, and that it is listening? (`aiteWorker.status()` today; there is no popup yet. **Write down what you could not tell.**) 2. Dispatch the long test (`npm run central -- dispatch --ref main --file scripts/long.aite ...`). Watch the tab open and the test run. 3. **Device switch.** Pause work at the device. Expected: the run stops at once, its tab closes, `status` says paused, and the central system will not send it anything else. Note how long it took and whether you were sure it had stopped. 4. Switch work on again. Expected: it listens again. 5. Dispatch the long test again. **Server stop.** Run `npm run central -- stop --reason "QA"`. Expected: within about 15 seconds (a heartbeat) the run stops and the tab closes, and nothing new starts. `resume`, and check that a job waiting runs. 6. **Revoke.** Run `npm run central -- device revoke ID`. Expected: the device stops listening within seconds and `status` says it must be enrolled again. Pass: each of 3, 5 and 6 ended the work in under 20 seconds and you were not left unsure whether work was still happening. Fail: any run kept going, or a switch that you could not tell had worked. ## B. Origin approval, for a build with a list of sites (pass bar: a job cannot touch a site until you, at the device, have said yes to it) This part is for an extension built with `--allow` (a list of sites fixed in the build). An open build has nothing to approve; its control is part G. Built in #68. An origin is usable only when three things agree: the extension was built for it, the central system enrolled the device for it, and **you approved it at the device**. Nothing is approved after enrolling. **There is no popup yet** (the toggles in part A are the same: they are `aiteWorker` calls from the extension's page console until packaging, #62, gives them a screen), so you approve with `aiteWorker.setApprovedOrigins([...])` and look with `aiteWorker.origins()` or `aiteWorker.status()`. 1. Enrol a device, then run `aiteWorker.origins()`. Expected: `asked` lists the sites you are about to allow, `approved` is empty. 2. Dispatch a test on one of those sites before approving. Expected: refused before anything opens, with `origin-not-approved` and the site's name; no tab opens; the dispatcher says the device is passed over (`has not approved it`) once it has told the central system. 3. Approve it (`aiteWorker.setApprovedOrigins(["https://..."])`). Expected: within seconds the central system sees the origin in the device's capabilities and the next test runs. 4. Take it back (`aiteWorker.setApprovedOrigins([])`) while a test is running on it. Expected: the run ends within seconds and the central system gives the job to another device. 5. Enrol the device again with a new code. Expected: approval is empty again (a new enrolment asks again). 6. Try to approve a site the extension was not built for, or the service did not enrol the device for. Expected: refused, naming the site. 7. Write down whether, as the person at the device, the list of sites and the wording of the refusals were enough for you to decide, and what you would want a popup to say. Pass: 1 to 6 behave as expected and you were never unsure what was approved. The popup itself is not tested until it exists. ## C. Spending (pass bar: a test that spends cannot run until you, at the device, have switched spending on) Built in #69. A script that spends says `spends` on a line of its own; a job needs a spends allowance (`--spends N` when you dispatch); and **you** must switch spending on at the device. It is off after enrolling, and it is not the same switch as posting. Like the toggles above it is an `aiteWorker` call from the extension's page console until packaging (#62) gives it a screen. Use a testnet wallet and a test script only. 1. Run `aiteWorker.status()`. Expected: `allowSpending` is false. 2. Dispatch a test whose script says `spends` without `--spends`. Expected: the central system refuses to make the job and says the test spends and the job allows no spends. 3. Dispatch it with `--spends 1`. Expected: no job is made, and the answer says the device has spending switched off (the device tells the central system its switches, #75). Nothing opens a tab. If you name a device that has not told the central system anything, the device itself refuses (`script-refused`). 4. Switch posting on (`aiteWorker.setAllowPosting(true)`) and dispatch again. Expected: still no job. Posting does not switch spending on. 5. Switch spending on (`aiteWorker.setAllowSpending(true)`) and dispatch again. Expected: the test runs. 6. Dispatch the spending test four times in a day on one device. Expected: the fourth is recorded as skipped with the spend cap's reason (3 a day per device by default; 5 across devices). 7. Enrol the device again. Expected: `allowSpending` is false again. 8. Write down whether it was clear to you, at the device, what you had switched on, and what you would want a popup to say before it lets money move. Pass: 1 to 7 behave as expected and nothing spent before step 5. ## D. Which profile ran it (pass bar: a test cannot run in a session that is not the one its label stands for, and you can see which profile did run) Built in #70. Chrome gives an extension no profile name, so the extension makes up an id for the browser profile it is in. Use two browser profiles if you can. 1. Run `aiteWorker.status()` in each profile. Expected: `profile.id` is set, stays the same after you restart Chrome, and differs between the two profiles; `profile.accounts` lists the labels and accounts you set. 2. Sign in to the site as the account set for the label, and run a test that names that label. Expected: the test runs, and `archive.json` in the result says `profile` with the label, the account, `session: "verified"` and the id. 3. Sign out (or sign in as another account) in that browser profile and run the same test. Expected: it fails at its first step (`wrong-session`) before anything is clicked, and the message says which account was found and how to renew the session. 4. Run it on a site whose first page does not show who is signed in. Expected: it runs, and `archive.json` says `session: "unknown"`. This is the intended behaviour (decided 2026-10-16): aite is a test utility, so how strict a site is about who is signed in is up to the site under test. 5. Write down whether the id is enough for you to tell your profiles apart, or whether you want the Chrome profile name shown (it cannot be read without another permission). Pass: 1 to 3 behave as expected. ## E. Posting limits (pass bar: nothing posts more than you allowed, and no side door gets round the switch) Built in #71. Use a test that says `posts` and a testnet or throwaway account. 1. With posting switched off at the device, make a job by hand for it with `POST /admin/jobs` (or `npm run central -- job create`). Expected: refused with 409, "posting switched off", and no job exists. 2. Switch posting on, then run `aiteWorker.setPostsPerDay(1)`. Run one posting job. Expected: it runs, and `aiteWorker.status()` shows `postsToday: 1`. 3. Offer a second posting job to the same device. Expected: the device refuses it ("this job allows no posts" style reason) even though the job and the server allow posts. A job that does not post still runs. 4. Make jobs by hand until the system cap is reached. Expected: the next one is recorded as skipped with the cap named, in the results, never offered. 5. Write a test that clicks a button called "Publish" and leaves out `posts`. Expected: refused with the line number and the advice to add `posts`. 6. Write down whether a limit of 10 posts a day is a sensible default for you. Pass: 1 to 5 behave as expected. ## F. The installed extension (pass bar: you can put it on a second device, enrol it, and see that nothing a page does can reach your switches) Built in #62. Follow docs/runbook-laptop.md with a laptop, and write down every place the runbook was wrong or unclear. 1. Build, copy, tunnel and load the extension. Expected: it loads with no error on `chrome://extensions`, and the icon opens a popup that says it is not enrolled. 2. Enrol with a code. Expected: the popup says Enrolled and shows you as the person, the device id, the central address and a profile id; `npm run central -- device list` shows the device. 3. With a build that has a list of sites: before you tick anything, dispatch a test. Expected: refused for the origin (nothing opens); tick the site, save, dispatch again, and it runs. With an open build there is nothing to tick: the test runs (see part G). 4. Close all Chrome windows (or let Chrome stop the extension), wait a minute, run `npm run central -- device list`. Expected: the device is back online without you opening the popup. 5. Open one of the sites a test may touch and its developer console. Try `chrome.runtime.sendMessage("EXTENSION_ID", {method: "setAllowSpending", args: [true]})`. Expected: it fails (no answer), and spending is still off in the popup. 6. Write down what the popup should say or show that it does not, and whether the tunnel was a bother. Pass: 1 to 5 behave as expected. ## G. An open extension (pass bar: any site can be tested without a new download, and a site you list is never touched) Built after #62. The extension built with no `--allow` tests any site a signed job names, except the sites you put on its list. Enrol it with `npm run central -- enrol --any-site ...`. 1. In the popup (an open build shows a box titled Sites this device will not test, and no site boxes), check the box is empty. Dispatch a test on a site that was never mentioned anywhere when the extension was built. Expected: it runs, with nothing for you to approve. 2. Open that site yourself in an ordinary tab in the same browser profile. Expected: nothing of aite touches that tab (no debugging bar, no change to the page). 3. Put the site's host in the box (a line such as `example.com`) and Save. Dispatch the same test. Expected: the central system names your device as one that does not allow tests on that site and sends it elsewhere (or says no device matches); if you made the job by hand for this device, it is refused. No tab opens. 4. Start a longer test, and while it runs add the site it is on to the box. Expected: the run ends within seconds. 5. Take the site off the list. Expected: the next test on it runs again. 6. Try a line that is not a site (for example `not a host!`). Expected: the popup says which line is not a site and keeps the old list. 7. Fetch the archive of a test that ran on the laptop (`npm run central -- job archive ID --out FILE`) and open `device.json`. Expected: the browser version, the operating system, the time zone and the local time are the laptop's, not balrog's. `npm run central -- job show ID` lists where the service saw the device connect from (loopback through a tunnel). 8. Write down which sites you would put on the list before trusting this on your everyday profile, and whether the Chrome install prompt (it can read and change data on all sites) matches what you expected. Pass: 1 to 7 behave as expected. ## Results Fill in a table: step, what happened, seconds, anything unclear, pass or fail. Commit this file with the results below it. | Step | What happened | Seconds | Unclear? | Pass | |---|---|---|---|---| | | | | | |