31 KiB
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.mjscallback: asubthat is not inconsole.sso.providers[].subjectsrefusessubject-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 incli.mjsover the human transport (busCalleroverHUMAN_CLI).accessContextFor(packages/cli/src/access.mjs:62) returnspeers: [].
Build items
- Granted-user sign-in (sso.mjs).
createSsotakes an optionalusershook: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 carriesuser: <id>andname: <id>. A miss on both refusessubject-unknown, as today (the log line names the provider, iss and sub; no new code is needed for the refusal). - The access hook (new
packages/webui/src/access.mjs).consoleAccess({ load, business }):loadis() => loadAccess(accessFilePath(configDir()), accessContextFor(system, env)), R1's loader, called on every use, never cached.find({provider, sub}): the user whosesubjectshold that pair, with at least one grant in the Console's business thatcoversat levelviewer(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.businessresolves asbusCallerdoes:--business, else the live host state.
- 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 logssso refused code=user-revoked provider=<id> user="<id>". 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. - A user-cli transport per session (new user-bus.mjs, cli.mjs).
userCall({ socket, business, user })spawnsUSER_CLI(packages/bus/src/user-cli.mjs) as a direct child of the Console withMOSAIC_BUS_USER_NONCEset, 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 setsMOSAIC_BUS_CLI_NONCE(it strips one from the inherited env) and never runsHUMAN_CLI.startServertakesuserOptions(session), a factory that builds the whole bus-backed option set (consoleBus, new console-bus.mjs) over that session's user transport, per request. - 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 403owner-onlybefore any bus call. Behind that gate, a user request never holds a human-transport object: the handler swaps every bus-backed option (bus,chat,sendand 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 answersnot-configured.GET /serves a separate page,user.htmlwithuser.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.
- the chat routes (
- The notice.
user.htmlholds 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." - Hub label and peer links.
/api/configreturnsfederation: {self, peers: [{label, origin}]}fromsystem.federationwhen set, for owner and user sessions alike. Both pages show "This Console: " and one plain<a href="<origin>">label</a>per peer, built with textContent andsetAttribute('href'); no script, no fetch to a peer, no proxy (the hub proxy route is R8's). - F8 peers wiring.
accessContextForreturnspeers: (system.federation?.peers ?? []).map((p) => p.id), and the ACCESS_USAGE text and the function's comment drop "peer homes are not read yet". - Bus boot wiring (packages/cli/src/config.mjs).
bootConfigaddsaccess: { file: accessFilePath(configDir(env)), context: accessContextFor(system, env) }when the file exists (lstat). No file: noaccesskey, so no user binds, as before R6. A context that throws: a warning and noaccess. A file the loader refuses at boot (sage-3153): one warning line, which the host writes to stderr asmosaic-bus: warning: access file refused at boot (<code>), no granted user binds until it is fixed: <path>. 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, agot "<value>"), 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) isaccess-unreadable, and anything else isaccess-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/messagethrough the owner'ssendover 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 (
coversatviewer, 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
accessboot 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'saccessis 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
userOptionsfactory over a realBrokerwithbindUserand anaccessfunction, and a separate transport test checks the spawn: theUSER_CLIpath, 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
pluginsread goes over the owner transport (accepted, sage-3153).createChatasks the buspluginsfirst (createChannelCaller), andpluginsis not a user verb, so a user chat would refuse before its first read.createChat({ call, owner })sendsownerexactlypluginswith{}, and every other verb goes overcall. consoleBus passesownerthrough, and cli.mjs gives itownerCall. 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
pluginswith{}; - 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 andagentCallermoved with it; the tester socket still builds its own. -
C9. 401 and 403 from the broker. chat-http's STATUS maps
access-refusedto 403 andaccess-revokedto 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:
pluginswith{}, 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'sowner(chat.mjs) and userOptionsFor (console-bus.mjs), wired withownerCallin 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)
- 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).
- C6 is accepted under conditions: the plugins answer stays server-side, the owner transport
carries only
pluginswith{}, one G test (G9) records both transports, and a mutant (M8) routing one more chat verb over the owner is killed. - C7 is accepted, with the follow-up note under C7.
- 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/configsaysuser,GET /is the user page, the chat lists reach the broker asuser:<id>. - G2 an ungranted
subis refused: a user in the file with no grant, a grant for another business, an expired grant, and a sub in no file; each issubject-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-revokedlog line and no transport call. Restoring the grant does not bring the session back, andGET /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-onlyand 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-refusedfrom the broker. The only write is the broker's own refusal evidence: exactly oneaction.refusedevent 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, andpluginsnever 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/configcarries self and the peers' labels and origins; the user and owner pages show "This Console: hub" and an<a>per peer with the configured href and label (browser test, sso-browser.test.mjs style). - G8 the transport:
userCallover 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.fileandcontext.peersset; alice (home a configured peer) binds; bob getsaccess-revoked. - B2 without the file: no
accesskey; alice getsaccess-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, withaccess-invalid,access-invalidandaccess-unreadableand 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'ssendfor 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, <the access refusal line>], 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.jsat 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 oneaction.refusedper 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 withaccess-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:
warningsalso 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.