morgana: row 193 design r3 (duckworth-0008, sage-1368): messages private, run governance owner-visible with a read table and pin test (B1), message filter moves to 193b1 (B2), N1-N4

This commit is contained in:
2026-10-20 14:19:47 -05:00
parent 19c015741d
commit 76f4605e19
+70 -40
View File
@@ -1,10 +1,10 @@
# Row 193 design: many humans per business, human roles, private and shared threads
Morgana (Opus 5.5), 2026-10-20. Revision 2: sage-1354 (5.7 private from the owner, 193b split) and gizmoduck-0018 (coverage and the R4 predicate in FED r4.1). Gitea #1691, queue row 193, brief `docs/plans/2026-10-20_human-users.md`
Morgana (Opus 5.5), 2026-10-20. Revision 3: duckworth-0008 B1, B2 and N1-N4, sage-1368. Revision 2: sage-1354 (5.7 private from the owner, 193b split) and gizmoduck-0018 (coverage and the R4 predicate in FED r4.1). Gitea #1691, queue row 193, brief `docs/plans/2026-10-20_human-users.md`
(blob f7472d1e47b1). Reviewer Duckworth, gate owner Sage. Base: `refactor` at 53bb59462.
Read for this design: COMMON.md (53bb59462), the brief, Gizmoduck's federation design r3.1 (a2840a6be on
gizmoduck/federation-design, cited as FED), Duckworth's federation reviews (b402197ff, 03a663cd7,
Read for this design: COMMON.md (53bb59462), the brief, Gizmoduck's federation design r4.4 (b67bc6de9 on
gizmoduck/federation-design, cited as FED; r3.1 a2840a6be was read first), Duckworth's federation reviews (b402197ff, 03a663cd7,
45cbe5896 on duckworth/reviews), Sage's Q175 SSO design (`agents/sage/work/q175-sso/DESIGN.md`), the row 192
and row 196 briefs, Darkwing's row 190 packet (uncommitted, sha256 5493c9df3ec0...), and the bus, business,
cli and webui code at the lines cited. Messages: morgana-0001 to 0003, sage-1338, gizmoduck-0009,
@@ -202,7 +202,9 @@ CREATE INDEX thread_shares_thread ON thread_shares (thread, target, seq);
-- no_update, no_delete and no_replace triggers, as thread_opens has (schema.sql:319-321).
```
- `target` is an owner string: `user:<id>`, the business human's name, or `*`.
- `target` is an owner string: `user:<id>`, the business human's name, or `*`. A share whose target is a
name that is no longer the business's `human` (after a rename) is void: rule 1 in 5.4 compares against
the current owner string, so it matches nobody (Duckworth N4).
- `by` is the sharer's owner string, which is always the thread's owner.
- A share's state is its latest row per `(thread, target)` (Duckworth N12, REVIEW-r3.md:74-76). Shares
are not assumed to only grow.
@@ -269,35 +271,61 @@ Gizmoduck asked that a grant loss end access to threads shared with that user (g
- A user who loses coverage of their own thread also loses its shares: rule 3 in 5.4.
The downgrade case. With viewer, "no live grant covers" would be wrong for runs: a viewer grant covers the
thread but must not let a run continue. FED r4.1 (73677b87d, gizmoduck-0018) puts this in R4: `revoked`,
claim end and the launch check call `covers` with level `collaborator`, so a downgrade to viewer ends the
run within one launcher tick. Row 193 doesn't carry that change. 193b1 still has a downgrade test and a
mutant (section 8), because viewer is new in this row.
thread but must not let a run continue. FED puts this in R4: `revoked`, claim end and the launch check call
`covers` with level `collaborator`, so a downgrade to viewer ends the run within one launcher tick. FED r4.4
(b67bc6de9, mos-1020) chains R4 after row 193's viewer level (193a) and keeps the level-loss test and its
mutant in R4. Row 193 carries neither (Duckworth N3), so the mutant has one owner.
### 5.7 What "private" means, stated plainly
Sage ruled (sage-1354): a user's thread is private from the owner on the bus too. Jason's words are "private
agent sessions per user", and in Case 2 the owner of Melanie's hub is not the user whose thread it is.
Duckworth's B1 asked where the line sits, since a user-thread run acts with the owner's role authority (F10).
I took Duckworth's option (a): **the user's messages are private, and the run's governance is not.** The
run spends the owner's authority, so its decisions, task writes and claims are the owner's to see and
answer. Filtering them too (option b) would need a thread link on `decisions`, more schema, and would hide
from the owner what is done in the owner's name.
- A user's thread is private from every other user and viewer at the bus boundary. They reach the bus only
through the Console and the row 196 verifier, and every read is filtered.
- It is private from the owner at the bus boundary as well. Row 193 adds no owner verb that lists or reads
another human's threads, the Console doesn't show them, and the two board reads that return messages by
id, `trail` and `replies`, filter them (below).
- The filter. A message is hidden from a session when it is in a thread (`message_threads`) whose owner is a
`user:<id>` other than the caller and `#readable(s, t)` is false. Threads owned by the business human or
by `tester` are not affected, so row 177's behaviour is unchanged.
- `trail`: drops hidden messages, their `deliveries` rows, and `events` rows whose `subject` is a hidden
message id. A `subject` that is itself a hidden message returns `message-not-found`, as an unknown id
does, so the reply doesn't confirm the id exists.
- `replies {id}`: a hidden `id` fails `message-not-found`, and hidden messages are dropped from the
result.
- The filter applies to every session that can call these verbs: the owner, reader sessions and the
tester.
- What it does not do. The owner holds the store as the instance's OS user and can read it with sqlite, and
the thread's instance runs with the owner's role authority (F10). That is the same-uid limit in section 6,
not a bus path. An owner audit verb, explicit and logged, is a follow-up row only if Jason asks for one
(sage-1354).
Every read an owner or reader session has, and its side of the line:
| Read | Shows | Side |
|---|---|---|
| `thread.list`, `thread.messages`, `thread.open` | own threads only, plus shares to the owner from 193b2 | private: other users' threads never appear |
| `trail` | events, messages, decisions and deliveries by subject | messages filtered (below); decision rows and their events stay visible |
| `replies` | messages replying to an id | filtered (below) |
| `messages` | messages `to_role='human'` | private by construction: a user-thread run can't send to the human (`user-thread-human`, FED 3.6 item 7) |
| `inbox` | open decisions routed to the human | governance, visible |
| `decisions {list: roles \| recent}` | decision rows with question, options and recommendation | governance, visible |
| `agents` | role claims and each held role's state | governance, visible |
| `tasks`, `state` | tracker tasks and launch state | governance, visible |
| `credentials`, `usage` | credential state, usage records | governance, visible; neither holds message text |
| `project.list`, `names` | projects and instance names | business-wide for the owner, visible |
So a decision a user-thread run raises shows in the owner's `inbox` or `decisions`, with the free text the
run wrote. That text can quote the user. 5.7 doesn't call it private. The users' help text (R5) and the
Console's share and thread pages say so: "Decisions your thread's agent raises go to the business owner."
The gated-raise route. A user-thread run's gated raise routes to the human at broker.mjs:1683, the same
route the tester guard blocks (`tester-thread-human`). Under option (a) that is intended: the owner answers
decisions made with the owner's authority. Whether FED adds a guard there is FED 3.6's call, and Duckworth has
raised it with Gizmoduck. If FED adds one, row 193 follows it and this table changes for `inbox` only.
The message filter. A message is hidden from a session when it is in a thread (`message_threads`) whose owner
is a `user:<id>` other than the caller, and `#readable(s, t)` is false. Threads owned by the business human or
by `tester` are not affected, so row 177's behaviour is unchanged.
- `trail`: drops hidden messages, their `deliveries` rows, and `events` rows whose `subject` is a hidden
message id. A `subject` that is itself a hidden message returns `message-not-found`, as an unknown id does,
so the reply doesn't confirm the id exists.
- `replies {id}`: a hidden `id` fails `message-not-found`, and hidden messages are dropped from the result.
- It applies to every session that can call these verbs: the owner, reader sessions and the tester.
- It lands in 193b1, with no schema (Duckworth B2). Before 193b2 there are no shares, so `#readable` is false
for anyone but the thread's owner and b1 hides every message in another user's thread. 193b2 adds only the
share exception. So no build order can put a user thread live without the filter.
What it does not do. The owner holds the store as the instance's OS user and can read it with sqlite. That is
the same-uid limit in section 6, not a bus path. An owner audit verb, explicit and logged, is a follow-up row
only if Jason asks for one (sage-1354).
## 6. The bus human boundary (brief point 5)
@@ -352,18 +380,17 @@ the instance's uid.
- Rollback after shares are written: the same restore loses shares made since the window. Grants are in
access.json and survive. Users re-share. Row 181's restore guard counts `thread_shares`, so the loss is
shown, not silent.
- To keep that window small, the build splits (section 9): 193a and 193c have no schema change and can go
live in any window. 193b carries 3j.
- To keep that window small, the build splits (section 9): 193a, 193b1, 193c and 193d have no schema
change and can go live in any window. 193b2 carries 3j.
## 8. Tests and mutants
Bus, each in `packages/bus/tests/` (193b1: viewer, mixed grants, downgrade, board reads; 193b2: shares, migration, the 5.7 filters):
Bus, each in `packages/bus/tests/` (193b1: viewer, mixed grants, board reads, the 5.7 message filter and governance pin; 193b2: shares, the share exception to the filter, migration):
- Viewer: each allowed verb works, every other verb refuses `access-refused`, including `thread.create`,
`thread.open`, `thread.send` and `thread.share`.
- Mixed grants: collaborator on project A and viewer on B. Create in A works, create in B refuses.
- Downgrade: collaborator to viewer ends the user's claims, sets `revoked` in `threadsState`, and the
launcher starts no run.
- Downgrade: not here. FED R4 owns the level-loss test and its mutant (r4.4, mos-1020, N3).
- Shares: owner shares with `user:<id>`, the user reads `thread.messages` and gets `thread-not-owned` on
`thread.send` and `thread.open`. Share with `*` reaches every covered user and the owner. Share with a user
whose grant doesn't cover the project or instance refuses `share-not-covered`.
@@ -375,10 +402,13 @@ Bus, each in `packages/bus/tests/` (193b1: viewer, mixed grants, downgrade, boar
- Non-owner `thread.share`, `thread.unshare`, `thread.shares` refuse. Tester refuses on all three.
- `thread.list` plain shape unchanged. `thread.list {shared: true}` returns only readable threads.
- Board reads refuse users and viewers.
- `trail`, for the owner, a reader session and the tester: a user's unshared thread message as `subject`
- Governance pin (B1, sage-1368): a user-thread run raises a decision; it shows in the owner's
`decisions recent` with its question text, and `inbox` lists it when routed to the human, while the
user's message bodies stay hidden from the owner's `trail` and `replies`.
- `trail`, for the owner, a reader session and the tester (193b1): a user's thread message as `subject`
fails `message-not-found`; a decision trail that reaches such a message omits it, its deliveries and its
events. Once the thread is shared with the owner, the owner's trail shows it.
- `replies`, same three sessions: a hidden `id` fails `message-not-found`; hidden replies are dropped. The
- `replies`, same three sessions (193b1): a hidden `id` fails `message-not-found`; hidden replies are dropped. The
owner's own and the tester's threads are unchanged.
- Migration: a 3i store with threads migrates to 3j, row counts unchanged, `thread_shares` empty and
append-only (update, delete, replace each abort). Restore-guard and `COUNTED` include it.
@@ -396,7 +426,7 @@ Mutants, each must be killed:
2. Rule 2 dropped (a share widens the grant).
3. Rule 3 dropped (a revoked owner's thread stays visible).
4. Latest-row lookup replaced by "any share row" (unshare ignored).
5. `revoked` predicate back to "any level covers" (viewer keeps a run alive).
5. (Moved to FED R4, N3: the level-loss mutant is R4's.)
6. Viewer allowed `thread.send`.
7. `#threadRead` used for `thread.send` (sharee writes).
8. `share-not-covered` check removed.
@@ -410,15 +440,15 @@ Mutants, each must be killed:
| Row | Work | Chains on | Schema |
|---|---|---|---|
| 193a | R1 level check widened to viewer | FED R1 candidate | none |
| 193b1 | Bus: viewer verb list, per-grant level checks, `revoked` predicate, users refused on board reads | FED R3, R4 candidates, row 196 verifier | none |
| 193b2 | Bus: `thread_shares`, share verbs, `#readable`, `#threadRead`, the 5.5 filters, the 5.7 `trail` and `replies` filters | 193b1 candidate | 3j |
| 193b1 | Bus: viewer verb list, per-grant level checks, users refused on board reads, the 5.7 `trail` and `replies` filter with `#readable` as owner-only | FED R3, R4 candidates, row 196 verifier | none |
| 193b2 | Bus: `thread_shares`, share verbs, the share rules of `#readable`, `#threadRead`, the 5.5 share filters | 193b1 candidate | 3j |
| 193c | `mosaic access` user verbs and `--level viewer`, if R5 lacks them | FED R5 candidate | none |
| 193d | Console: shared list, share controls, viewer read-only | FED R6, row 192 candidates, 193b2 for share controls | none |
Sage ruled the split (sage-1354): 193b1 has no schema change and can go live first. 193b2 carries 3j, and
Sage takes its migrate window to Mos when 193b2 is near. The 5.7 filters go in 193b2 because they use
`#readable`. Until 193b2 lands no user thread exists live (FED R8 is later), so nothing is exposed in
between.
Sage takes its migrate window to Mos when 193b2 is near. The 5.7 message filter is in 193b1 (Duckworth B2,
sage-1368), so it is live before any user thread can be, whatever order FED R8 and 193b2 land in. 193b2 adds
only the share exception. FED R4 chains on 193a (viewer level), and 193b1 chains on R4.
Coverage, settled in FED r4.1 (73677b87d, gizmoduck-0018, still under Duckworth's review). A grant's
`projects` is `["*"]` or bus project UUIDs (`projects.id`, broker.mjs:2306; `threads.project` references it).