Files

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 level collaborator (USER_LEVEL). A viewer-only user was refused access-revoked at bindUser and on every request.
  • #granted and #grantedThread asked collaborator for every user verb, reads included.
  • The reader session's board list was an inline array at the request gate.

The change

  1. VIEWER_VERBS = ['project.list', 'thread.list', 'thread.messages', 'names'], exported. userLevel(verb) gives viewer for these and collaborator for the other user verbs (thread.create, thread.open, thread.send). USER_LEVEL is gone.
  2. #userGrants keeps a live grant at any level (covers(..., {level: 'viewer'})), so a viewer binds and passes the request gate. The verb check against USER_VERBS is unchanged and still comes first.
  3. #granted(s, verb, project, instance) and #grantedThread(s, verb, t) take the verb and ask userLevel(verb). Each handler passes its own verb name. A viewer gets access-refused on create, open and send, the code design 4.3 names.
  4. BOARD_VERBS exports the reader list unchanged (inbox, agents, thread.claims, tasks, trail, decisions, credentials, replies, messages, state, usage), and the reader gate uses it. None is in USER_VERBS, so every user is refused them access-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)

  1. VIEWER_VERBS is the ruled list and sits inside USER_VERBS.
  2. A viewer binds; project.list, names, thread.list and thread.messages answer what its grant covers (alice downgraded, bob viewer from the start).
  3. 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 #thread refuses a non-owner thread-not-owned before the level check; open and send are asked on alice's own thread. Bob stays the viewer who never held a collaborator grant.
  4. Every verb off USER_VERBS refuses a viewer access-refused before its own checks.
  5. 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).
  6. 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.
  7. Governance pin, user side: one row per item of the 5.7 sentence (decisions, inbox entries, claims as agents and thread.claims, tasks as tasks and state, project files as project.files and project.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.