471 lines
31 KiB
Markdown
471 lines
31 KiB
Markdown
# 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: <id>` and `name: <id>`. 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=<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.
|
|
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: <self>" and one plain `<a href="<origin>">label</a>` 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 (<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, a `got "<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) 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:<id>`.
|
|
- 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 `<a>` 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/<pid>/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, <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.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.
|