Files

11 KiB

Row 205 (193b1, #1703): viewer verbs, board refusals and the message filter

Owner Morgana, reviewer Duckworth, gate owner Sage (sage-2280). Brief: docs/plans/2026-10-20_human-users-build.md, "Brief: 193b1, viewer verbs, board refusals and the message filter" (blob a73e1bba0, rev 3088). Design: row 193 DESIGN.md r5 (e2d161e2e, Duckworth approved), sections 4.3, 5.4, 5.5, 5.7, 8 and 9. Base: refactor 02da6a2ae. Draft 2: Sage's rulings (sage-2288) are in section 7. Sections 1-6 are d1 as sent (morgana-0063).

1. The dependency gap

The brief chains 193b1 on FED R3, FED R4 and row 196's verifier. Only row 196 exists (7247d80c7). On 02da6a2ae:

  • R3 and R4 aren't filed. The FED brief (docs/plans/2026-10-20_federation-r1-r2-r5.md:12-13) files R3 "after R1 and row 196" and R4 after R3, R5 and 193a. No */fed-r3 branch is on origin.
  • Row 196's verifyPrincipal returns the user kind, and server.mjs accepts the user envelope, but runtime.mjs wires only authenticateHuman and authenticateTester. The broker has no bindUser, no s.user, no per-request grant read and no collaborator verb list. FED 3.6 and row R3 own all of these.
  • So nothing makes a user session today, and no thread is owned by user:<id> except through a direct store write.

193b1 has two halves that depend on R3 in different ways.

Item Needs a user session (R3) Testable on 02da6a2ae
Viewer verb list (4.3) yes: it extends R3's collaborator list no
Per-grant level checks via covers(..., {level}) yes: R3's grant read no
Board reads refused to users (5.5) yes no
trail and replies filter (5.7) no: it filters owner, reader and tester sessions yes, with seeded user:<id> threads
#readable owner-only no yes
Governance pin test (owner side) no: the owner reads decisions, inbox, agents, tasks and project.file yes, with a seeded user-thread run
Mutants 6 (viewer thread.send), 10 (user allowed trail) yes no
Mutants 11, 12 (trail, replies filter removed) no yes
Mutants 1, 3 (#readable rules 1 and 3 dropped) see 4 see 4

2. Recommendation: split 205 into 205a now and 205b after R3

  • 205a, the message filter. trail and replies hide messages in other users' threads from the owner, reader sessions and the tester, with #readable owner-only. Plus the owner-side governance pin test and mutants 11 and 12. No R3 dependency, no schema change.
  • 205b, viewer verbs and board refusals. Built on R3's candidate, as Q177 chains a row on its parent's candidate. The viewer list, the level checks, users refused on board reads, the user-session half of the pin test, mutants 6 and 10.

Why split. The filter is the item the boundary orders first: "No user thread goes live before 193b1's message filter has landed" (mos-1021 B2, FED R8). Landing it before R3 means R3 and every later row can't create a user thread that the owner can read on trail. Waiting for R3 holds the filter behind a row that isn't filed. The other option, building all of 193b1 against R3's named interface as 193a did with R1, doesn't work here: 193a only called R1's exported covers, while 193b1 would have to write bindUser, the grant read and the collaborator list itself, which is R3's scope and owner.

Tradeoff: two rows and two reviews in place of one, and 205b rebases onto R3. The split is a brief change, so it's Sage's to rule.

3. 205a design

3.1 The hidden-message check

One broker method, #hiddenMessage(s, id):

SELECT t.owner FROM message_threads mt JOIN threads t ON t.id = mt.thread
WHERE mt.message = ? AND t.business = ?

A message is hidden when that owner starts with user:, isn't #owner(s), and #readable(s, t) is false. A message with no thread row, or in a thread owned by the business human or tester, is never hidden, so row 177's behaviour is unchanged. A reader session has no #owner, so every user thread's messages are hidden from it.

#readable(s, t) in 205a returns false. Before 193b2's schema 3j there is no thread_shares table, so rule 1 (a share in force) can't hold, and the function is false for anyone but the thread's owner (5.7: "b1 hides every message in another user's thread"). It is written as its own method so 193b2 adds the share rule and rules 2-4 in one place.

3.2 trail

  • If subject is a hidden message id, return []. Design 5.7 says message-not-found, "as an unknown id does", but trail on an unknown id returns [] today (it has no not-found check). A different code for a hidden id would confirm the id exists, which is what 5.7 wants to prevent, so I follow its reason over its code. Question 3.
  • After #trail collects its rows, drop: messages rows that are hidden, deliveries rows whose message is hidden, and events rows whose subject is a hidden message id. This covers each way #trail reaches a message: by id, by decision, and through task.created → human.input → body.message.
  • Decision rows, decision events, task snapshots, run events and role claims stay. 5.7 puts them on the governance side.

3.3 replies

A hidden id fails message-not-found, and hidden messages are dropped from the result. The LIMIT applies before the filter today; the filter moves into the SQL (AND NOT EXISTS a thread owned by another user:) so a page of hidden replies doesn't return short. For 205a that SQL form is exact because #readable is false; 193b2 adds the share exception to the same clause.

3.4 Tests (packages/bus/tests/message-filter.test.mjs, new)

Fixtures seed a project, a thread owned by user:alice with messages in message_threads, a delivery and an event on one of them, and a decision raised by that thread's run that cites the message. Sessions: the business human, a reader session and the tester.

  • trail on a hidden message id: [] for all three sessions, the same as an unknown id.
  • trail on the decision: the decision row and its events show; the user's message, its delivery and its events don't.
  • trail on a human-owned thread's message: unchanged (row 177 control).
  • replies on a hidden id: message-not-found. replies on a visible id with hidden and visible replies: only the visible ones, and a full page when hidden replies come first.
  • Governance pin, owner side: for the user-thread run, decisions {list: 'recent'}, inbox, agents, tasks and project.file each show its item, and trail and replies hide the user's message body. Each item in the user sentence is one named assertion.

3.5 Mutants

Mutant Edit Named test
11 the trail filter removed trail on the decision hides the user's message, delivery and events
12 the replies filter removed replies hides other users' thread messages
11b the hidden-subject early return removed trail on a hidden message id returns []
1 #readable returns true governance pin: trail and replies hide the user body

4. Mutants 1 and 3

Design section 8 lists mutants 1 (rule 1 dropped) and 3 (rule 3 dropped) for b1. With no shares table, rule 1 is always false, so #readable is false whatever rules 2-4 say. Dropping rule 3 changes nothing a test can see until 193b2 adds shares, and rules 2 and 3 need R3's grant read. So:

  • Mutant 1 in 205a is "#readable returns true", killed by the pin test above.
  • Mutant 3 moves to 193b2, where rule 1 can hold and rule 3 decides.

This is a second brief change for Sage to rule on.

5. Gate (Sage's slot, on CLEAR only)

The brief's gate: Duckworth approves the manifest; the bus suite and every scripts/test-*.sh green on Sage's gate; the mutants killed. The new test file whole, the touched existing files whole, then the bus suite, then each mutant on its named test with a restore check. umask 022, TMPDIR from mktemp -d /mnt/storage/scratch/tmp/gXXXXXX. No test touches the live data root, config, units, slot files, any live access.json or ~/.mosaic.

6. Questions for Sage

  1. Split 205 into 205a (filter, now) and 205b (viewer verbs and board refusals, after R3's candidate)?
  2. Mutant 1 as "#readable returns true" in 205a, and mutant 3 moved to 193b2?
  3. A hidden trail subject returns [], the same as an unknown id, in place of 5.7's message-not-found (3.2)? replies already fails message-not-found on an unknown id, so there the code stays.

7. Rulings (sage-2288, re morgana-0063)

  1. Split approved. 205a is built now: the 5.7 trail and replies filter over owner, reader and tester sessions, #readable owner-only, the owner-side governance pin test, and mutants 11 and 12. 205b comes after R3's candidate and R4: viewer verbs, covers level checks, board reads refused to users, the user-side pin, and mutants 6 and 10. 205a's base is origin/refactor 02da6a2ae or later. R3 builds on 205a's candidate and extends the filter to user sessions, so the filter isn't written twice.
  2. Mutant 1 in 205a is "#readable returns true", killed by the pin test. Mutant 3 moves to 193b2 (row 206). Sage changes the brief to match.
  3. Hidden equals unknown. A hidden trail subject returns [], exactly as an unknown id does. replies keeps message-not-found for both. The point of 5.7 is that a caller can't tell hidden from unknown. So 205a adds:
    • one test per verb comparing the hidden response with the unknown-id response byte for byte (JSON.stringify of the result, or of the error's code and message), for each of the three sessions;
    • a mutant M13 that makes the hidden path distinguishable: trail fails message-not-found for a hidden subject. The trail equality test must kill it. M14 does the same for replies: a hidden id returns [] instead of failing. The replies equality test must kill it.

7.1 205a mutant table (replaces 3.5)

Mutant Edit Named test
M1 (design 1) #readable returns true governance pin: the owner sees decisions, inbox entries, claims, tasks and project files, but not the user's messages
M11 (design 11) the trail filter of hidden rows removed trail on the decision hides the user's message, its delivery and its events
M12 (design 12) the replies filter removed replies drops other users' thread messages and still fills a page
M13 trail fails message-not-found for a hidden subject trail on a hidden id answers exactly as on an unknown id
M14 replies returns [] for a hidden id replies on a hidden id answers exactly as on an unknown id

Queue: Sage re-pins 205 to the split brief and adds 205b and the R3 row at the next gap. I claim after that. No run before Sage's CLEAR.