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-r3branch is on origin. - Row 196's
verifyPrincipalreturns theuserkind, and server.mjs accepts theuserenvelope, but runtime.mjs wires onlyauthenticateHumanandauthenticateTester. The broker has nobindUser, nos.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.
trailandreplieshide messages in other users' threads from the owner, reader sessions and the tester, with#readableowner-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
subjectis a hidden message id, return[]. Design 5.7 saysmessage-not-found, "as an unknown id does", buttrailon 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
#trailcollects its rows, drop:messagesrows that are hidden,deliveriesrows whosemessageis hidden, andeventsrows whosesubjectis a hidden message id. This covers each way#trailreaches a message: by id, bydecision, and throughtask.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.
trailon a hidden message id:[]for all three sessions, the same as an unknown id.trailon the decision: the decision row and its events show; the user's message, its delivery and its events don't.trailon a human-owned thread's message: unchanged (row 177 control).replieson a hidden id:message-not-found.replieson 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,tasksandproject.fileeach show its item, andtrailandreplieshide 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 "
#readablereturns 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
- Split 205 into 205a (filter, now) and 205b (viewer verbs and board refusals, after R3's candidate)?
- Mutant 1 as "
#readablereturns true" in 205a, and mutant 3 moved to 193b2? - A hidden
trailsubject returns[], the same as an unknown id, in place of 5.7'smessage-not-found(3.2)?repliesalready failsmessage-not-foundon an unknown id, so there the code stays.
7. Rulings (sage-2288, re morgana-0063)
- Split approved. 205a is built now: the 5.7
trailandrepliesfilter over owner, reader and tester sessions,#readableowner-only, the owner-side governance pin test, and mutants 11 and 12. 205b comes after R3's candidate and R4: viewer verbs,coverslevel checks, board reads refused to users, the user-side pin, and mutants 6 and 10. 205a's base is origin/refactor02da6a2aeor later. R3 builds on 205a's candidate and extends the filter to user sessions, so the filter isn't written twice. - Mutant 1 in 205a is "
#readablereturns true", killed by the pin test. Mutant 3 moves to 193b2 (row 206). Sage changes the brief to match. - Hidden equals unknown. A hidden
trailsubject returns[], exactly as an unknown id does.replieskeepsmessage-not-foundfor 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.stringifyof 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:
trailfailsmessage-not-foundfor a hidden subject. Thetrailequality test must kill it. M14 does the same forreplies: a hidden id returns[]instead of failing. Therepliesequality test must kill it.
- one test per verb comparing the hidden response with the unknown-id response byte for byte
(
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.