# Row 294 build notes (Launchpad): FED R6, Console granted-user sign-in (#1800) Brief: docs/plans/2026-10-23_mvp-readiness-rows.md, "Brief: FED R6, Console granted-user sign-in". Design: agents/gizmoduck/work/federation/DESIGN.md at b93f098f0 (r4.11, sha256 1bcee8df…c8e6), sections 3.1, 3.4, 3.7 and the R6 row of section 7. Assigned in sage-3136. Reviewer Duckworth. ## Base and chain Branch launchpad/row294 on origin/refactor 253346e3c (270+271, schema 3n in packages/bus, and 571). Parents done: 192 (#412 phase 1), R1 (201), R2, R3 (274). Row 208 and R7 (296) wait on this row. ## Plan ### What the Console does today - `sso.mjs` callback: a `sub` that is not in `console.sso.providers[].subjects` refuses `subject-unknown`. A session record is `{provider, iss, sub, name, ...}` and every signed-in request runs as the business human. - `serve.mjs`: every bus route, chat route and write uses objects built once in `cli.mjs` over the human transport (`busCaller` over `HUMAN_CLI`). - `accessContextFor` (packages/cli/src/access.mjs:62) returns `peers: []`. ### Build items 1. **Granted-user sign-in (sso.mjs).** `createSso` takes an optional `users` hook: `users.find({provider, sub})` returns a user id with a live grant, or null. The callback asks it only after the owner subjects miss, so the owner path is unchanged. A user session record carries `user: ` and `name: `. A miss on both refuses `subject-unknown`, as today (the log line names the provider, iss and sub; no new code is needed for the refusal). 2. **The access hook (new `packages/webui/src/access.mjs`).** `consoleAccess({ load, business })`: - `load` is `() => loadAccess(accessFilePath(configDir()), accessContextFor(system, env))`, R1's loader, called on every use, never cached. - `find({provider, sub})`: the user whose `subjects` hold that pair, with at least one grant in the Console's business that `covers` at level `viewer` (the bus's own liveness test, `#userGrants`). A loader throw finds nobody (fail closed). - `check(session)`: the same test for the session's user and its own `(provider, sub)`. A user whose subject was removed, whose grants were all removed or expired, or whose file now refuses fails it. - `business` resolves as `busCaller` does: `--business`, else the live host state. 3. **Per-request re-read (sso.mjs `session()`).** For a user session, `users.check(session)` runs after the expiry check, before any route. A failure deletes the session record and logs `sso refused code=user-revoked provider= user=""`. The request then goes the signed-out way: 401 (`sign-in required`) for /api/ and writes, or the sign-in redirect for a page GET. The cookie is not cleared on that response; it names a session that no longer exists, so it signs nothing in, and the next sign-in replaces it. 4. **A user-cli transport per session (new user-bus.mjs, cli.mjs).** `userCall({ socket, business, user })` spawns `USER_CLI` (packages/bus/src/user-cli.mjs) as a direct child of the Console with `MOSAIC_BUS_USER_NONCE` set, so user-cli does not re-exec and the broker's parent check (principal.mjs: parent argv[1] is webui/src/cli.mjs, argv[2] `serve`) sees the Console. The request on stdin is `{business, user, verb, args}`. It never sets `MOSAIC_BUS_CLI_NONCE` (it strips one from the inherited env) and never runs `HUMAN_CLI`. `startServer` takes `userOptions(session)`, a factory that builds the whole bus-backed option set (`consoleBus`, new console-bus.mjs) over that session's user transport, per request. 5. **Owner-only controls hidden (serve.mjs and a user page).** A user session reaches only: - the chat routes (`/api/chat/*`), over its own user transport, and - `GET /api/config`, `/healthz`, `/auth/*`, and the user page's static files. Every other path answers 403 `owner-only` before any bus call. Behind that gate, a user request never holds a human-transport object: the handler swaps every bus-backed option (`bus`, `chat`, `send` and each write, `processing`, project files) for the same object built over that session's user transport, and nulls the ones with no bus behind them (`reads`, the board proxy), so with the gate removed an owner route still reaches the broker only as the user, or answers `not-configured`. `GET /` serves a separate page, `user.html` with `user.js`, that renders only projects, threads and messages (no board, inbox, agents, settings, editor, processing or Sign-in-as-owner controls). The owner page's assets are not served to a user session. The gate is one function, `userRoute(path, method)`, so the M1 mutant removes one call. 6. **The notice.** `user.html` holds row 193's sentence word for word in the thread view: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages." 7. **Hub label and peer links.** `/api/config` returns `federation: {self, peers: [{label, origin}]}` from `system.federation` when set, for owner and user sessions alike. Both pages show "This Console: " and one plain `label` per peer, built with textContent and `setAttribute('href')`; no script, no fetch to a peer, no proxy (the hub proxy route is R8's). 8. **F8 peers wiring.** `accessContextFor` returns `peers: (system.federation?.peers ?? []).map((p) => p.id)`, and the ACCESS_USAGE text and the function's comment drop "peer homes are not read yet". 9. **Bus boot wiring (packages/cli/src/config.mjs).** `bootConfig` adds `access: { file: accessFilePath(configDir(env)), context: accessContextFor(system, env) }` when the file exists (lstat). No file: no `access` key, so no user binds, as before R6. A context that throws: a warning and no `access`. A file the loader refuses at boot (sage-3153): one warning line, which the host writes to stderr as `mosaic-bus: warning: access file refused at boot (), no granted user binds until it is fixed: `. The access stays wired, so the broker refuses each user bind and the owner's bus boots. R1's loader has no refusal codes, and its messages can quote the file (a JSON parse error, a `got ""`), so the line never carries the message. The code comes from the loader's exit code instead: 4 (the file could not be opened as a regular file, including a symbolic link) is `access-unreadable`, and anything else is `access-invalid`. config.mjs now imports access.mjs, which imports config.mjs (`loadSystem`). The cycle is safe because neither module touches the other's exports at module top level, only inside functions (sage-3153). ### Choices and departures (for Sage and Duckworth) - **C1. A separate user page, not the owner SPA with sections hidden.** "Hidden" here means never sent: the owner page's markup and scripts are not served to a user session. Hiding sections in app.js would ship the owner controls and rely on CSS. Tradeoff: a second small page to keep. - **C2. The Console's gate is an allowlist of user routes**, so a new owner route added later is owner-only by default. - **C2b. No human transport behind a user session.** Without the swap, M1 would send `/api/bus/message` through the owner's `send` over human-cli, and the bus would accept it as the owner. The swap is what makes "the bus refusal stands on its own" true. - **C3. Liveness is the bus's test** (`covers` at `viewer`, in the Console's business), so the Console and the broker agree on who is live. The broker still decides every verb, project, instance and thread (design 3.1 D4). - **C4. The bus's `access` boot option is wired here (sage-3142, Q2; accepted, sage-3153).** Build item 9 above. Sage's ruling: with the file, users bind; without it, none; with a bad file, it "refuses as R1's loader does"; a boot never invents a grant. My reading of "refuses": the broker's `access` is R1's loader, read on every user request, so a bad file refuses every user bind (`access-revoked`) and the owner's bus still boots. Refusing the whole boot would take the owner's bus down over a user file. Tests B3 pin this reading. No live unit or config changes. - **C5. Tests use an in-process Broker behind the user transport seam.** The broker's user principal needs the Console as the user-cli's parent process, which a node:test process is not. So the webui tests inject a `userOptions` factory over a real `Broker` with `bindUser` and an `access` function, and a separate transport test checks the spawn: the `USER_CLI` path, the user nonce variable, no human nonce, and the `{business, user, verb, args}` request. R7 runs the real Console process end to end. - **C6. The chat's `plugins` read goes over the owner transport (accepted, sage-3153).** `createChat` asks the bus `plugins` first (createChannelCaller), and `plugins` is not a user verb, so a user chat would refuse before its first read. `createChat({ call, owner })` sends `owner` exactly `plugins` with `{}`, and every other verb goes over `call`. consoleBus passes `owner` through, and cli.mjs gives it `ownerCall`. createChannelCaller (plugins/src/channel.mjs:16-20) uses the answer only to gate the webui channel, server-side. Sage's conditions, each pinned by G9: - the plugins answer never appears in a user's response; - in a user's chat the owner transport carries only `plugins` with `{}`; - every chat verb goes over the user transport. Mutant M8 routes one more chat verb over the owner transport, and G9 kills it. A new broker user verb would have been a policy change, so it is out. - **C7. The user transport is a new file, user-bus.mjs, not a mode in bus.mjs.** bus/tests/channel-proof.test.mjs pins the sha256 of webui/src/bus.mjs (with human-cli.mjs, human.mjs and cli/src/transport.mjs). Changing bus.mjs would mean an old-test change through Mos. user-bus.mjs repeats the spawn and framing code (about 40 lines). bus.mjs is byte-identical to the base. Tradeoff: two copies of the spawn plumbing. Accepted (sage-3153). **Follow-up (not this row):** the next row that re-pins the channel-proof hash folds userCall back into bus.mjs and drops the copy. - **C8. console-bus.mjs.** The bus-backed option set moved out of cli.mjs into `consoleBus(call, {send, plugins})`, so the owner and each user session build the same set the same way. The agent verb lists and `agentCaller` moved with it; the tester socket still builds its own. - **C9. 401 and 403 from the broker.** chat-http's STATUS maps `access-refused` to 403 and `access-revoked` to 401, so a broker refusal reaches the browser as the right status. ## Exception: the owner transport in a user's chat (C6; mos-1394, sage-3157) Duckworth rules on this directly. The same text goes in the manifest notes. > In a granted user's chat the Console uses the owner's transport for one call: `plugins` with > `{}`, the channel gate read. Nothing else goes over it. Its answer stays in the Console and is > never sent to the user. No value from the user's session or request reaches the call. Mos ruled > in mos-1394 that this is not an access-rule change, because the Console reads its own channel > gate, and that it holds only while nothing from the user reaches the call. Every chat verb goes > over the user's own transport. Where: createChat's `owner` (chat.mjs) and userOptionsFor > (console-bus.mjs), wired with `ownerCall` in cli.mjs. Pinned by G9 and mutants M8, M10 and M11. > If this reads as owner authority used for the user, it goes back to Mos. ## Rulings (sage-3153) 1. Boot wiring is accepted: a bad file refuses every user bind and the owner's bus boots. Added: one stderr line at boot naming the path and a code, never the contents (item 9, B3, M9). The mutual import is fine with no top-level use (item 9). 2. C6 is accepted under conditions: the plugins answer stays server-side, the owner transport carries only `plugins` with `{}`, one G test (G9) records both transports, and a mutant (M8) routing one more chat verb over the owner is killed. 3. C7 is accepted, with the follow-up note under C7. 4. Owned files are accepted. packages/cli/src/access.mjs overlaps Morgana's row 295 (527002f97), and merge-tree is clean. 294 lands first and 295 rebases after it. packages/cli/tests/ access.test.mjs is now unchanged: the peer test moved to access-boot.test.mjs, so the only overlap is access.mjs. Ruling sage-3157 (mos-1394): the boot line is accepted as built, with no codes from R1. For C6, G9 asserts the owner call is exactly `plugins` with exactly `{}`, and that no value from the session or the request reaches it. Mutants M10 (session value) and M11 (request value) are killed by G9. The exception is written above and goes in the manifest notes. The per-session builder moved into console-bus.mjs (`userOptionsFor`), so the wiring G9 runs is the same code cli.mjs uses, and a session value added there is a mutant G9 can kill. ## Owned files - cli: src/config.mjs, src/access.mjs, tests/access-boot.test.mjs (new) - webui src: sso.mjs, access.mjs (new), console-bus.mjs (new), user-bus.mjs (new), chat.mjs, chat-http.mjs, cli.mjs, serve.mjs - webui public: user.html (new), user.js (new), federation.js (new), index.html, app.js - webui tests: granted-user.test.mjs (new) - this BUILD.md No old test changes. Hash pins checked: `git grep` over 'packages/*/tests/*' for each touched path with a 64-hex pin found only channel-proof's bus.mjs pin, which C7 avoids. ## Tests New file packages/webui/tests/granted-user.test.mjs, on Sage's fake OP (tests/helpers/fake-oidc.mjs) and an in-process Broker with an access function, like bus/tests/user-principal.test.mjs: - G1 a granted user signs in: session cookie set, `/api/config` says `user`, `GET /` is the user page, the chat lists reach the broker as `user:`. - G2 an ungranted `sub` is refused: a user in the file with no grant, a grant for another business, an expired grant, and a sub in no file; each is `subject-unknown`, no cookie, nothing bound. - G3 revoked mid-session: sign in, then remove the grant, remove the subject, expire the grant, or make the loader refuse. The next chat read is 401 with a `user-revoked` log line and no transport call. Restoring the grant does not bring the session back, and `GET /` redirects to sign-in. - G4 owner verbs: for each owner route (board reads under /api/bus/*, settings, names, instances, routes, agents, processing, project files, /api/bus/message, /api/board), a user session gets 403 `owner-only` and the transport ran zero times. - G5 the bus refusal stands alone: the same session's user transport, called directly with each owner verb, gets `access-refused` from the broker. The only write is the broker's own refusal evidence: exactly one `action.refused` event per verb, the count taken from the verb list's length (22 today), each body exactly `{ code: 'access-refused' }`, and every other table's row count unchanged (round 2; run 1 placed the round 1 "events unchanged" pin as a test defect, sage-3194). The broker's `#refused` (bus broker.mjs:4423) writes the code alone and no verb, so the test can't match each event to its verb; pinning the whole body means a field added there shows up here. - G9 the C6 seam (sage-3153, sage-3157), with the test's userOptions built by userOptionsFor as cli.mjs builds them: a user session runs the chat's project list, thread create, list, open, send and messages. The owner transport (a recording `ownerCall`) saw only `['plugins', {}]`. Each chat verb went over the user transport, and `plugins` never did. A marker in the owner's answer appears in no response. Every owner call is exactly `['plugins', {}]`, and none of alice, alice-sub, the thread name, the message body, the project id or the thread id appears in what the owner transport received. - G6 the notice: the user page's notice element text equals the sentence exactly. - G7 hub label and peer links render from config: `/api/config` carries self and the peers' labels and origins; the user and owner pages show "This Console: hub" and an `` per peer with the configured href and label (browser test, sso-browser.test.mjs style). - G8 the transport: `userCall` over a recording unix socket in a scratch dir. While the child waits for the answer, the test reads /proc//cmdline (argv[1] is USER_CLI), environ (MOSAIC_BUS_USER_NONCE set; MOSAIC_BUS_CLI_NONCE absent though the parent env passed one) and stat (ppid is the test process, so user-cli did not re-exec). The request line is `{verb, args, user: {business, user, nonce, pid}}`, which user-cli builds from `{business, user, verb, args}`. packages/cli/tests/access-boot.test.mjs (sage-3142), over bootConfig and startBroker in a scratch root, the boot config through a JSON round trip as over the host IPC: - B1 with access.json: `access.file` and `context.peers` set; alice (home a configured peer) binds; bob gets `access-revoked`. - B2 without the file: no `access` key; alice gets `access-revoked`. - B3 with text that is not JSON, a home that is not a configured peer, and a symbolic link: the bus boots and alice gets `access-revoked`. The warnings are exactly one line, with `access-invalid`, `access-invalid` and `access-unreadable` and the path, and none quotes the file (a marker string in the file is absent from the line). access-boot.test.mjs also holds the F8 peer test: a grant whose user's home is a configured peer id validates; an unconfigured peer id refuses `home must be a configured peer`. Old tests: the sso.test.mjs and serve tests stay as they are. If one needs a change, it goes to Mos through Sage first (mos-1272). ## Mutants | # | Change | Killed by | Expected | |---|---|---|---| | M1 | remove the `userRoute` gate call in serve.mjs | G4 (:239, first route answers 403 `access-refused`, not `owner-only`) | G4 red at :239 on its first route; G5 green in the same run | | M2 | skip `access.check(session)` per request | G3 | red | | M3 | change one word of the notice in user.html | G6 | red | | M4 | `peers: []` back in accessContextFor | the F8 peer test and B1 (access-boot.test.mjs) | red | | M5 | sign-in skips the grant test (any listed user signs in) | G2 | red | | M6 | userCall sets MOSAIC_BUS_CLI_NONCE or runs HUMAN_CLI | G8 | red | | M7 | bootConfig drops `access` | B1 | red | | M8 | chat.mjs routes `thread.list` over `owner` too | G9 | red | | M9 | accessFor drops the boot refusal line | B3 | red | | M10 | userOptionsFor passes `owner: (v, a) => owner(v, { ...a, sub: session.sub })` | G9 | red | | M11 | chat.mjs keeps the last request's args in `channel()` and passes them to `owner('plugins', …)` | G9 (thread create's `{project, name}` reaches owner) | red | M1 is the brief's "owner verb still refused by the bus": with the Console gate gone, the owner verb reaches the broker over the user transport and the broker refuses it (`access-refused`, 403). G4 kills M1 at :239 on its first route, because it pins the Console's own code `owner-only`. The count and "nothing written" asserts at :242-244 come after the loop, so under M1 they never run; the run records G4's failure with the actual value (`access-refused`). The bus refusing on its own is shown by G5, green in the same M1 run. (Corrected per Duckworth C1, duckworth-0249b; the earlier row said G4's "refused, nothing written" stayed green, which cannot be observed under M1.) ## Run limits Each run is `timeout -k 10 LIMIT`. TMPDIR comes from `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`. A timeout with no failed test goes to Sage first. A timeout in the shell phase goes to Mos unplaced (mos-1275). Under sage-3187, every LIMIT is at least 3x the longest recorded run, set before any run. The order is the webui, cli and bus suites whole, then each scripts/test-*.sh, then the mutants. | Run | Longest recorded | Receipt | LIMIT | |---|---|---|---| | webui suite | 57 s | gizmoduck queue-264 BUILD.md:474 (row 278 run 3) | 600 s | | cli suite | 682 s | gizmoduck queue-270 BUILD.md:660 (7328da8ca) | 2100 s | | bus suite | 25 s | gizmoduck queue-270 BUILD.md:658 | 900 s | | scripts/test-auth.sh | 1.9 s | 3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json | 126 s | | scripts/test-conductor.sh | 8.1 s | same | 145 s | | scripts/test-config.sh | 3.2 s | same | 130 s | | scripts/test-discord.sh | 8.8 s | same | 147 s | | scripts/test-extension-package.sh | 3.0 s | same | 130 s | | scripts/test-foundation.sh | 49.4 s | same | 269 s | | scripts/test-land-work.sh | 16.9 s | same | 171 s | | scripts/test-queue.sh | 35.2 s | same | 226 s | | scripts/test-release.sh | 12.2 s | same | 157 s | | scripts/test-task.sh | 36.8 s | same | 231 s | | each mutant probe (its killing file alone) | none: granted-user.test.mjs and access-boot.test.mjs are new | the first green run in the suites sets the record | 300 s | The script limits are Gander's SHELL-LIMITS values (3x the longest recorded run, plus 120 s), as recorded in agents/gander/work/queue-276/BUILD.md. If a new file's suite time is over 100 s, its mutant LIMIT goes to 3x that time before the probes run. ## Review Round 1, manifest 0b3f9c9d…94f3 (ca710eabd): Duckworth APPROVE (static), duckworth-0249b, note agents/duckworth/work/row294/ROUND1-ca710eabd.md on duckworth/reviews 5b7089ea1. The owner-transport exception is accepted and does not go to Mos. - C1: the M1 row and note above are corrected. The M1 run records G4's :239 failure value and G5. - C2: the gate runs on Sage's CLEAR. - N1 (no test reaches the C2b swap): no M12 in this row. Adding one needs a new test and a new manifest round after approval; the brief does not require it. Left as a follow-up for Sage to place: a test that a user session's options carry no owner caller besides `plugins`, with an M12 where serve.mjs keeps the owner's `send` for a user session. - N2, N3: accepted as they stand; no change. - N4: the user-bus.mjs fold-back (C7) is proposed to Sage as a queue row, for the next channel-proof re-pin. Round 2 (Mos mos-1412 through Sage sage-3195; for Duckworth): two test changes after run 1, no product change. The manifest adds packages/webui/tests/browser.test.mjs (18 paths). - Red 1, old test, Mos approved (a): browser.test.mjs:22 pins index.html's script list. It gains '/federation.js' at index 2, where 294's index.html:7 puts it, ahead of brand.js. The :21 comment adds row 294 and federation.js. No other assertion in the file changes. Old and new lines are in ASSERTION-PRESERVATION.json. (b), loading the federation view another way, was rejected. - Red 2, new test G5: the pin is the one in the G5 note above. Mos's suggestion is taken: the count is `refusals.length`, not a literal. The verb check is not possible, because the event names no verb; the test pins the whole body instead. Round 3 (sage-3212; goes to Mos as the row's second red, sage-3211, before any push or run): one changed line, in new test G5. Its baseline was `SELECT max(seq) n FROM events`. The G5 fixture has an empty events table, so that is NULL and `seq > NULL` matches no row: run 2 read 0 events added where run 1's count(*) had shown 22. The line is now `SELECT coalesce(max(seq), 0) n FROM events`. Nothing else in G5 and no product file changes. The manifest stays 18 paths; only granted-user.test.mjs's hash moves. My round 2 check missed the empty-table case. Round 4 (Mos mos-1419 through Sage sage-3238, after run 3's third red): two test files, no product file. The manifest adds packages/cli/tests/stack-release.test.mjs (19 paths). - stack-release.test.mjs, an old test: the rehearsal fixture copies a fixed list of config.mjs's imports into each scratch copy. 294's config.mjs imports access.mjs, which imports transport.mjs (transport.mjs imports only errors.mjs). The :1991 list gains both, with a row 294 comment at :1987. No assertion changes. The real copy carries all of packages/cli/src (stack-copy.sh COPY_PATHS), so the product was not affected. - access-boot.test.mjs, new test B1-B3: the shared helpers.mjs business sets tracker.baseUrl without tracker.project, so bootConfig warns about it before any access line (trackerFor runs before accessFor in config.mjs). A TRACKER constant holds that line. B1 and B2 pin `[TRACKER]`. B3 pins `[TRACKER, ]`, and the secret-marker check reads every warning joined. boot() still records every warning. Mos chose a full pin over a filter: it also catches a second tracker line, a reordering, and config.mjs:102's context warning. I proposed a startsWith('access file') filter; Sage found it would hide that :102 warning. - My miss: B1-B3 were new and had not run before the gate. ## Runs ### Run 1, 2026-10-23 04:57Z (sage-3187 CLEAR), head 685ff053b, manifest 0b3f9c9d…94f3 Before the run, the branch was rebased from 253346e3c onto c902a767d. `git range-diff -s` showed all 6 commits "=", and the stable patch-ids matched. The head was pushed as launchpad/row294-on-c902a767d 024499fc2. TMPDIR was under /mnt/storage/scratch/tmp. npm ci --ignore-scripts ran at the root only. | Step | Result | Time | Limit | |---|---|---|---| | webui suite | 365 tests: 362 pass, 2 fail, 1 skip | 55.8 s | 600 s | The driver stopped at the first red, so the cli, bus, scripts and mutants did not run. The slot was returned (launchpad-0060). - browser.test.mjs:22, an old test, pins index.html's script list. 294 adds `/federation.js` at index 2. It went to Mos through Sage (sage-3192); I made no change before the ruling. - G5:262, a new test, failed `22 !== 0`: the broker writes one `action.refused` per refusal. This was placed as a test defect under mos-1272 and accepted by Sage (sage-3194). It is fixed in round 2. - The skip was the editor rebuild check (editor-provenance.test.mjs:162), which needs esbuild 0.28.2 from packages/webui's own lockfile. An environment fix: the next run also runs npm ci --ignore-scripts in packages/webui (sage-3194). - The last landed whole-webui gate skipped the same test: row 264a's run, webui 345 pass of 346 with 1 skipped, "the esbuild 0.28.1 editor rebuild" (gizmoduck queue-264 BUILD.md:516-518, DEFERRED 264a N2). Row 270's gate ran only webui pinned-copy.test. The fresh 294 gate still runs npm ci in packages/webui first, so the test should run here. If it skips again, the run names it and says why. ### Run 2, 2026-10-23 05:33Z (sage-3199 CLEAR), head 66c181717, manifest 7ec7b1ea…315b Duckworth's round 2 approve was recorded first (duckworth-0253, rev 3455, comment 31029). Drivers and logs are in run2/. TMPDIR was under /mnt/storage/scratch/tmp. | Step | Result | Time | Limit | |---|---|---|---| | npm ci --ignore-scripts, root | ok, esbuild 0.28.1 | 2.2 s | 900 s | | npm ci --ignore-scripts, packages/webui | ok, esbuild 0.28.2 | 1.2 s | 900 s | | webui suite | 365 tests: 364 pass, 1 fail, 0 skipped | 58.2 s | 600 s | The driver stopped at the first red. The slot was returned (launchpad-0070), and I made no edit. - The editor rebuild check ran and passed, and browser.test.mjs passed with the round 2 pin. - G5:269 failed `0 !== 22`, "one event per refused verb". Every verb was still refused with `access-refused`. Placed as a test defect (the NULL baseline, round 3 above); as the row's second red it went to Mos through Sage (sage-3211) before the fix runs. ### Run 3, 2026-10-23 05:55Z (sage-3231 CLEAR), head f26584089, manifest cb1648f3…78de Duckworth's round 3 approve was recorded first (duckworth-0255b, rev 3462, c31045). The drivers, summary, webui log and the cli failure section are in run3/. TMPDIR was under /mnt/storage/scratch/tmp. | Step | Result | Time | Limit | |---|---|---|---| | npm ci --ignore-scripts, root | ok | 2.2 s | 900 s | | npm ci --ignore-scripts, packages/webui | ok | 2.2 s | 900 s | | webui suite | 365 tests: 365 pass, 0 fail, 0 skipped | 58.2 s | 600 s | | cli suite | 909 tests: 889 pass, 20 fail, 0 skipped | 541.5 s | 2100 s | The driver stopped at the cli suite, so the bus suite, the scripts and the mutants did not run. No failure was compactions-launch, so the :229 rerun rule did not apply. The slot was returned (launchpad-0076), and I made no edit before Mos's ruling (round 4 above). - stack-release.test.mjs, 17 failures: R1-R7, R9-R12, R14, R15, row 233's --backup last test, and row196's rehearsal test at :2586 with its socket copy and legacy copy subtests. Each rehearsal failed at scratch-config: "Cannot find module …/packages/cli/src/access.mjs imported from …/packages/cli/src/config.mjs". My launchpad-0076 put the two subtests under row 233; they are row196's. - access-boot.test.mjs B1, B2 and B3: `warnings` also held the fixture's tracker line. ### Run 4, 2026-10-23 06:20Z (sage-3244 CLEAR, sage-3248 go), head 8616b9a4f, manifest a1ff5d67…9b99 Duckworth's round 4 approve was recorded first (duckworth-0257b, rev 3469, c31053). The run started at 06:20:53Z, on duckworth-0257b's relay of sage-3247; sage-3248 arrived seconds later with the same go. TMPDIR was /mnt/storage/scratch/tmp/gd3JQ8r. Receipts are in run4/: summary.txt, the drivers, suite-totals.txt (the three suites' node --test totals; the whole logs stay in scratch), every scripts/test-*.sh log, and each mutant's diff and TAP output with mutants.json. | Step | Result | Time | Limit | |---|---|---|---| | npm ci --ignore-scripts, root | ok | 1.9 s | 900 s | | npm ci --ignore-scripts, packages/webui | ok | 0.5 s | 900 s | | webui suite | 365 tests: 365 pass, 0 fail, 0 skipped | 57.9 s | 600 s | | cli suite | 909 tests: 909 pass, 0 fail, 0 skipped | 600.1 s | 2100 s | | bus suite | 873 tests: 873 pass, 0 fail, 0 skipped | 24.4 s | 900 s | | scripts/test-auth.sh | ok, 15 passed | 0.8 s | 126 s | | scripts/test-conductor.sh | ok | 6.8 s | 145 s | | scripts/test-config.sh | ok | 2.5 s | 130 s | | scripts/test-discord.sh | ok | 7.3 s | 147 s | | scripts/test-extension-package.sh | ok | 1.2 s | 130 s | | scripts/test-foundation.sh | ok | 39.1 s | 269 s | | scripts/test-land-work.sh | ok | 11.8 s | 171 s | | scripts/test-queue.sh | ok | 20.4 s | 226 s | | scripts/test-release.sh | ok | 2.7 s | 157 s | | scripts/test-task.sh | ok, 91 passed; the two live provider cases skip by design (row 244), no Docker group skipped | 13.9 s | 231 s | No step was rerun, so the :229 rule was not used. The worktree was clean after the scripts and after every mutant restore. Mutants, each its killing file alone under timeout -k 10 300, all killed (MUTANTS rc=0, 06:34:22Z): | # | Seconds | Red | |---|---|---| | M1 | 1.9 | G4 only; G5 ok in the same run | | M2 | 2.1 | G3 | | M3 | 1.8 | G6, and G7 (the page text changed under it) | | M4 | 0.5 | the F8 peer test, B1 | | M5 | 2.1 | G2, G3 | | M6 | 2.0 | G8 | | M7 | 0.3 | B1, B3 | | M8 | 2.1 | G1, G9 | | M9 | 0.4 | B3 | | M10 | 2.0 | G9 | | M11 | 2.0 | G9 | M1 record (Duckworth C1): G4's first failure is its first route, `GET /api/bus/inbox: 502 {"error":"access-refused","message":"access-refused"}`, expected 403 (run4/M1.tap). The failing assertion is granted-user.test.mjs:241, the loop's status check. The Mutants table above says :239; round 2's `tables` helper moved it down two lines. With the Console gate gone, the broker refuses the owner verb itself, and the Console answers 502 with the broker's code. G5, the bus refusing every owner verb on its own, is `ok 5` in the same TAP.