6.5 KiB
Row 275 (193b1b, #1780): viewer verbs and board refusals
Morgana, 2026-10-22, for Duckworth (reviewer) and Sage (gate owner). Brief: docs/plans/2026-10-20_human-users-build.md,
"Brief: 193b1b, viewer verbs and board refusals" (blob de3a6310b). Design: row 193 DESIGN.md r5 e2d161e2e
(morgana/row193), sections 4.3, 8 and 9. Base: refactor aca16404c, branch morgana/row275. Assignment: sage-2788.
What the code did before
#userGrants(broker.mjs, FED R3) kept only grants that cover at levelcollaborator(USER_LEVEL). A viewer-only user was refusedaccess-revokedatbindUserand on every request.#grantedand#grantedThreadaskedcollaboratorfor every user verb, reads included.- The reader session's board list was an inline array at the request gate.
The change
VIEWER_VERBS = ['project.list', 'thread.list', 'thread.messages', 'names'], exported.userLevel(verb)givesviewerfor these andcollaboratorfor the other user verbs (thread.create, thread.open, thread.send).USER_LEVELis gone.#userGrantskeeps a live grant at any level (covers(..., {level: 'viewer'})), so a viewer binds and passes the request gate. The verb check againstUSER_VERBSis unchanged and still comes first.#granted(s, verb, project, instance)and#grantedThread(s, verb, t)take the verb and askuserLevel(verb). Each handler passes its own verb name. A viewer getsaccess-refusedon create, open and send, the code design 4.3 names.BOARD_VERBSexports the reader list unchanged (inbox, agents, thread.claims, tasks, trail, decisions, credentials, replies, messages, state, usage), and the reader gate uses it. None is inUSER_VERBS, so every user is refused themaccess-refused. The test reads the same constant and pins it written out.
Downgrade path (design 4.3 note 2): a collaborator lowered to viewer still lists and reads its own threads
and can't open or send. The runtime's user transport calls bindUser, so a viewer can now bind over the
socket too. That is the intended effect; the verbs it reaches are the four above.
Not in this row: the share verbs, thread.list {shared: true} and #readable's share rules (193b2, row 206);
the revocation timer, access.reload and the level-loss test (FED R4, which mutant 5 moved to). FED R4 has
not landed; this row checks the level per request only, which is what R3 already reads per request.
Existing test changes (mos-1311 ruling)
Mos approved both changes in mos-1311 (16:44:56Z), under mos-1272. They follow row 193 DESIGN r5
(e2d161e2e, approved by Duckworth) section 4.3: the thread.list row at DESIGN.md:125 ("own threads still
covered (from an earlier collaborator grant)") and note 2 at :139 ("A viewer can still read its own
threads when a collaborator grant is lowered to viewer, but can't send or open them"). Condition 3: Duckworth
confirms in review that both match section 4.3.
All in packages/bus/tests/user-principal.test.mjs (FED R3, row 274):
| Test | Assertion | Old expectation | New expectation | Design line |
|---|---|---|---|---|
| "R3: bindUser binds a granted user and refuses one with no live grant" (title was "...no live collaborator grant") | bindUser with grant({ level: 'viewer' }) |
refused access-revoked (case 'viewer level' in the refused list) |
binds: assert.ok(user('alice'), 'viewer level'); the case leaves the refused list |
:125 viewer column; 4.3 viewer verbs need a bound viewer |
| "R3: a removed or expired grant refuses the next request access-revoked..." | thread.list after level: 'viewer' ('downgraded') |
access-revoked |
null (answers) |
:125 and :139 |
| same test, added (mos-1311 condition 1) | thread.send and thread.open on A after the downgrade |
(not asserted) | each access-refused; messages and thread_opens counts unchanged |
:139 "can't send or open them" |
The header comment of the file is updated to say a viewer grant binds. Every other R3 refusal (no grant,
another user, another business, expired, unreadable file, no file, removed, file refused) is unchanged.
No other test names a viewer grant on the bus (git grep for level: 'viewer' outside packages/business).
Tests, packages/bus/tests/viewer.test.mjs (new)
- VIEWER_VERBS is the ruled list and sits inside USER_VERBS.
- A viewer binds; project.list, names, thread.list and thread.messages answer what its grant covers (alice downgraded, bob viewer from the start).
- A viewer is refused thread.create, thread.open and thread.send
access-refused, and messages, threads and thread_opens counts are unchanged. Kills mutant 6 at alice's send. r3 (duckworth-0166 B1): bob is asked create only, as#threadrefuses a non-ownerthread-not-ownedbefore the level check; open and send are asked on alice's own thread. Bob stays the viewer who never held a collaborator grant. - Every verb off USER_VERBS refuses a viewer
access-refusedbefore its own checks. - Mixed grants: collaborator on project a, viewer on b. Create in a works, create in b refuses; send in b's old thread refuses, open refuses, messages reads the message sent while collaborator (r3, N1).
- Board reads (BOARD_VERBS, the project file verbs, limits, plugins, instances, routes) refuse alice
(downgraded viewer), bob (viewer) and carol (collaborator)
access-refused. Kills mutant 10. - Governance pin, user side: one row per item of the 5.7 sentence (decisions, inbox entries, claims as
agentsandthread.claims, tasks astasksandstate, project files asproject.filesandproject.file), each refused to a collaborator and a viewer while a run in carol's thread has raised a decision the owner sees; carol still reads her own message with thread.messages. The owner side stays in message-filter.test.mjs (row 205).
Mutants (design 8)
| # | Mutation | Killed by |
|---|---|---|
| 6 | 'thread.send' added to VIEWER_VERBS |
viewer.test 3 (send answers); test 1 pin fails too |
| 10 | 'trail' added to USER_VERBS |
viewer.test 6 (trail no longer access-refused); R3's exact USER_VERBS pin fails too |
The kill rule for each mutant names the behaviour test above, not the pin.
Gate (brief)
Duckworth approves the manifest. On Sage's gate: the touched test files whole (viewer, user-principal,
message-filter), the bus suite and every scripts/test-*.sh green, mutants 6 and 10 killed. broker.mjs
is imported by the cli, harness, business and seat suites as well; I'll propose them for the gate as in
row 277 (sage-2590). Nothing has run yet: built during Gander's 253 slot with no node.