32 KiB
Row 193 design: many humans per business, human roles, private and shared threads
Morgana (Opus 5.5), 2026-10-20. Revision 5: sage-1373 item 1 (mos-1023 item 2), the user sentence names every record the owner sees. Revision 4: duckworth-0011 B3 and N5. 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 f7472d1e47). Reviewer Duckworth, gate owner Sage. Base: refactor at 53bb59462.
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,
gander-0002 and gander-0005.
1. Summary
- Three human roles: owner, collaborator, viewer. The owner is the business file's one
human, as today. Collaborators and viewers are access.json users, the same store federation uses (FED 3.5). Their role is thelevelof a grant, so a role is per project and instance, not per person. - Every human principal is minted by one bus verifier, owned by row 196 (Gander). Row 193 states what it needs from that verifier (section 6) and adds no second mechanism.
- Threads stay private to their owner by default. A thread owner may share a thread, read-only, with a
named user, with the owner, or with everyone granted on it. Shares live in a new append-only bus table,
thread_shares. This is a deviation from brief point 4, agreed by Sage (sage-1338), for Duckworth to rule on (section 5.1). - A share never widens a grant. It counts only while the reader's live grant covers the thread's project and instance, and while the thread's owner still has coverage. Revocation, expiry and unsharing take effect on the next request.
- Existing businesses keep working unchanged: every existing thread is already owned by the business human, and an instance without access.json has exactly one human, the owner.
- The build splits so the part without a schema change can land first.
thread_sharesneeds schema 3j and a--migratewindow with Jason.
2. What exists today (measured at 53bb59462)
- One
humanper business (packages/business/src/business.mjs:160).bindHumanrefuses any other name (packages/bus/src/broker.mjs:532-538). - The owner's CLI proof is path-bound (#225):
verifyHumanchecks uid, start time, nonce (MOSAIC_BUS_CLI_NONCE),argv[1]equal to the tree'shuman-cli.mjs, then walks parents and refuses agent markers, launch records and agent binaries (packages/bus/src/human.mjs:64-96).authenticateHumanbinds the proof tobusinesses[business].human(packages/bus/src/runtime.mjs:175-182). - Threads:
threads(seq,id,at,business,project,instance,owner,name), append-only (packages/bus/schema.sql:247, triggers at :310-312).#owner(s)returnstesterors.human(broker.mjs:364-366).#threadrefuses a non-owner withthread-not-owned(:357-362). - Thread verbs are gated
if (!s.tester) this.#human(s):project.list(:2318),thread.create(:2325),thread.list(:2351, owner filter),thread.open(:2365),thread.send(:2374, writeshuman.inputfor the human only),thread.messages(:2401). - Board reads are business-wide and human or reader only:
inbox,agents,tasks,trail(:2143-2167),decisions(:2174),credentials(:2195),messages(:2217,to_role='human'),replies(:2422).trailandrepliesreturn any message in the business by id (:2679-2700, :2422-2432). - The Console reads threads by polling the same verbs (
packages/webui/src/chat.mjs:22-26). It has no stream, so a refusal on the next poll ends access. - The launcher restarts a thread with pending messages as the business human
(
packages/cli/src/launcher.mjs:533-542).
3. The human model (brief point 1)
3.1 Who the humans are
| Human | Where it is declared | How many | Identity |
|---|---|---|---|
| owner | business file human (unchanged) |
exactly one per business | the business human's name |
| user (collaborator or viewer) | access.json users[] with home: "local" (or a peer id under federation) |
any number | user:<id> |
| tester | not a human; row 177 test principal | one | tester |
The owner stays outside access.json (FED 9 Q2). A business has no second owner and no admin level. Adding one is out of scope; section 9 lists it as a follow-up for Jason.
3.2 How a sign-in or a call becomes exactly one human
| Entry | Becomes | Checked by |
|---|---|---|
Console SSO session whose (provider, sub) is in console.sso.providers[].subjects (row 192) |
the owner | the Console after OIDC checks, then the bus verifier (row 196) |
Console SSO session whose (provider, sub) is a users[].subjects entry in access.json with a live grant |
that one user | same |
| Console SSO session matching neither | nobody: refused | the Console, and the bus refuses any later claim |
CLI call through human-cli.mjs that passes the human proof |
the owner | the bus (verifyHuman) |
| Any other process, including an agent | nobody | the bus |
Why each is exactly one:
- The R1 loader refuses a
(provider, sub)pair that appears twice in access.json or also inconsole.sso.subjects(FED 3.5). So one subject maps to at most one human. - A user id can't be
human,testeror any business's human (FED 3.5). So a user can't be named as the owner. - A terminal maps only to the owner. The only identity the bus can verify for a local process is the instance's OS user, and that is the owner's. A user has no CLI path in row 193. This follows Mos's B1 rule (mos-0985): the principal comes from what the bus verifies, never the process's own claim.
- The bus never reads a user name from a request body as identity. The verifier returns the principal (section 6).
4. Human roles (brief point 2)
4.1 The three roles
- owner: everything the business human does today. Only the owner restructures the organization (business file writer, project and role changes) and only the owner manages users and grants.
- collaborator: FED 3.5's level, unchanged. Creates threads in granted projects on granted instances, sends into and opens its own threads, and in row 193 also shares its own threads.
- viewer: new. Reads threads shared with it, within its grant. It creates, opens and sends nothing, so it never drives an instance. FED 3.5 notes that a collaborator's message drives the instance with the owner's role authority (F10). A viewer has none of that.
The word "collaborator" stays, so FED's tests and help text need no rename. The brief's example "member" is this level.
A role is the level on a grant, and a grant is per business, project set and instance set. A user may
hold a collaborator grant on one project and a viewer grant on another. Every check asks "does a live
grant of a level that allows this verb cover this project and instance". A user has no single role.
4.2 R1 widening
FED R1 refuses any level but collaborator, because the 10-27 slice ships only that (FED 9 Q2).
Row 193 widens R1's level check to collaborator or viewer, with its own test that each is accepted and
anything else refuses. Nothing else in the access file changes. If R1 has not landed when row 193 builds,
this one-line widening goes into the row 193a candidate chained on R1's candidate (Q177).
4.3 Bus verbs per role
Every verb not in a user's column fails access-refused before its own checks, as FED 3.5 requires for
collaborators. That covers every owner verb, every board read, message.*, decision.*, project files,
spec.read, assistant.stop, launch.*, name.set, usage.* and access.reload.
| Verb | owner | collaborator | viewer |
|---|---|---|---|
project.list |
all | projects covered by any live grant | projects covered by any live grant |
thread.create |
yes | collaborator grant covers project and instance | refused |
thread.list |
own threads | own threads still covered | own threads still covered (from an earlier collaborator grant) |
thread.list {shared: true} (new) |
threads shared with the owner | threads shared with me and readable (5.4) | same |
thread.open |
own | own, collaborator grant still covers | refused |
thread.send |
own | own, collaborator grant still covers, no human.input |
refused |
thread.messages |
own, or shared with the owner | own (any level still covering), or shared and readable | same as collaborator |
thread.share, thread.unshare (new) |
own threads | own threads, collaborator grant still covers | refused |
thread.shares (new) |
own threads | own threads | refused |
names |
all | instances covered by any live grant | instances covered by any live grant |
| every other verb | as today | refused | refused |
Notes:
thread.listkeeps its response shape exactly. Shared threads come only fromthread.list {shared: true}, whose rows addowner. The Console and existing tests read the plain list unchanged.- A viewer can still read its own threads when a collaborator grant is lowered to viewer, but can't send or open them. This is the downgrade path.
- The tester keeps its row 177 verbs. It can't share or be shared with:
thread.share,thread.unshareandthread.sharesfailtester-refused, and a share target is nevertester. Agent testers (Q173) test sharing through the bus suites, not the live 7333 listener. readersessions (Console read caps, broker.mjs:1303) are the owner's board view and stay owner-only. A user never gets a reader session.
4.4 Console per role
- owner: as today, plus a share control on each own thread and a "Shared with me" list.
- collaborator: the chat view only, for granted projects (FED R6's granted-user sign-in), plus share controls on its own threads and "Shared with me". No board.
- viewer: "Shared with me" and its own old threads, read-only. No composer, no open, no share control.
- The Console hides controls a role can't use, but the bus check is authoritative. A Console bug can't widen a grant (FED 3.6). Each role gets a webui test with the UI check removed (mutant) that the bus still refuses.
4.5 Who manages humans and roles
Only the owner, through mosaic access (FED R5), which checks the human proof and refuses tester and agent
callers. Row 193 needs from R5:
mosaic access user add <id> --home local --subject <provider>:<sub>anduser remove <id>;--level viewerongrant;- the receipt line in
<dataRoot>/state/access-log.jsonland theaccess.reloadcall, as FED R5 has.
If R5 has no user verbs when row 193 builds, row 193c adds them on R5's candidate. The Console gets no
user or grant writes in row 193: a write from the Console would need a new owner verb that writes
access.json from the bus, and access.json is a config file only the owner's CLI writes. A read-only users
page for the owner is a follow-up (section 9).
The owner keeps every restructuring power (TARGET Case 1). Users can't touch the business file, projects, roles or access.json.
5. Private and shared threads (brief point 3)
5.1 Deviation from brief point 4, agreed by Sage (sage-1338)
Brief point 4 says access.json is the one store. Users, roles and grants live there. Per-thread shares do not, for three reasons (morgana-0002):
- F1. Thread ids are bus-generated UUIDs. A hand-typed share with a typo would fail the loader, and the loader refuses the whole file on any failure, which revokes every user.
- F2. Only the owner writes access.json. A collaborator could never share its own thread.
- F3. Threads are append-only bus rows, and a share is a runtime act by a principal that wants a trail.
So shares go in an append-only bus table, gated by the grant. Sage agreed (sage-1338). Gizmoduck had no objection (gizmoduck-0009) and recorded it as FED 9 Q2. Duckworth rules on it in this review.
5.2 Schema 3j
CREATE TABLE thread_shares (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
business TEXT NOT NULL,
thread TEXT NOT NULL REFERENCES threads(id),
op TEXT NOT NULL CHECK (op IN ('share','unshare')),
target TEXT NOT NULL,
by TEXT NOT NULL
) STRICT;
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).
targetis an owner string:user:<id>, the business human's name, or*. A share whose target is a name that is no longer the business'shuman(after a rename) is void: rule 1 in 5.4 compares against the current owner string, so it matches nobody (Duckworth N4).byis 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. - No existing row changes. Existing threads are already owned by the business human (
#ownerat broker.mjs:364-366), so they are the owner's and private. The brief's "all become the current human's" is already true.
The migration 3i-3j.sql creates the table, index and triggers. thread_shares joins every table list that
holds timed append-only tables: timedTables in store.mjs (:60-74), the broker's clock list
(broker.mjs:223-235), the restore guard's counted tables (row 181) and COUNTED in
scripts/stack-release.sh:315. The schema digest for 3j is pinned as the other versions are.
5.3 Verbs
thread.share {thread, to}: the caller must own the thread (thread-not-owned). For a user, a collaborator grant must still cover the thread.toisuser:<id>, the business human's name or*. Auser:<id>target must have a live grant, of any level, covering the thread's project and instance, elseshare-not-covered. Sharing with yourself failsinvalid-request. A share that is already in force failsshare-exists. Writes onesharerow.thread.unshare {thread, to}: the caller must own the thread. The share must be in force, elseshare-not-found. Writes oneunsharerow. Unsharing the owner's or*shares works the same way.thread.shares {thread}: the caller must own the thread. Returns the shares in force, latest row each.- Both writes go through
refuse()on failure, so each refusal recordsaction.refusedwithbyset (FED 3.6 item 6).
5.4 The read check, one function
#readable(s, t) decides whether a session may read thread t it doesn't own. True only when all hold:
- a share for
tis in force whose target is the caller's owner string, or*; - the caller is the owner, or the caller has a live grant of any level covering
t.projectandt.instance; - if
t.owneris a user, that user still has a live grant coveringt(a revoked owner hides the thread, sage-1338); t.owneris nottester.
#thread(s, id) keeps its owner check for thread.open, thread.send, thread.share, thread.unshare
and thread.shares. A new #threadRead(s, id) admits the owner or #readable, and serves only
thread.messages. A sharee fails thread-not-owned on every write verb.
5.5 Every read path and its filter
| Read path | Today | After row 193 |
|---|---|---|
thread.list |
owner=? |
unchanged for the owner; for users also each row's thread still covered |
thread.list {shared: true} |
none | rows where #readable holds |
thread.messages |
#thread |
#threadRead |
thread.open, thread.send |
#thread |
#thread, plus a collaborator grant for users |
project.list, names |
business-wide | per FED 3.5 for users, unchanged for the owner |
trail, replies |
human, reader or tester; any message by id | users refused (access-refused); for the others, messages in other users' unshared threads hidden (5.7) |
messages, inbox, agents, tasks, decisions, credentials |
human or reader | users refused (access-refused); owner unchanged. A user-thread run can't send to the human (user-thread-human, FED 3.6), so messages holds none of a user's thread |
message.receive by a run |
its own thread (:1933) | unchanged: runs read their own thread, never a share |
threadsState (launcher) |
all threads | adds revoked per 5.6 |
5.6 Revocation, expiry and unsharing
Gizmoduck asked that a grant loss end access to threads shared with that user (gizmoduck-0009 point 2).
- A sharee holds no claim and no run. Its access is only reads, and every read runs
#readableagainst the access state the broker already refreshes on each request (mtime, size, inode), on its 30 s timer, and onaccess.reload(FED 3.7). So a removed, expired or invalid grant ends share reads on the next request, with nothing to tear down. The Console polls, so a revoked sharee's next poll fails. - An unshare row ends access on the next request in the same way.
- 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 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.
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, message.receive, message.read |
messages to_role='human' (broker.mjs:1931-1936, :1953) |
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.files, project.file |
project files, including records a run writes with record.write (broker.mjs:1395-1407, :1441-1450) |
governance, visible |
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. Its role claims show in agents, its task writes in tasks, and a record it writes in
project.file. Any of these can quote the user, and 5.7 doesn't call them private. The users' help text
(R5) and the Console's share and thread pages say so in one sentence, which FED R6 reuses (mos-1023 item 2,
duckworth-0012 N21):
The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages.
The gated-raise route. A user-thread run's uncited gated raise or hard wait routes to the human (broker.mjs:1677-1680).
FED r4.5 (acd5f1f42, B9) settled this with no guard: the tester guard at :1683 (tester-thread-human) is not
copied, and the decision reaches the owner's inbox and no other (sage-1368, mos-1021, mos-1022). FED R3 tests it.
Row 193 adds nothing here.
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, theirdeliveriesrows, andeventsrows whosesubjectis a hidden message id. Asubjectthat is itself a hidden message returnsmessage-not-found, as an unknown id does, so the reply doesn't confirm the id exists.replies {id}: a hiddenidfailsmessage-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
#readableis 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)
Row 196 (Gander, #1694, brief docs/plans/2026-10-20_human-boundary.md at 96f243faf) owns P19 333 and the
verifier. Row 193 cites it and designs no boundary of its own (sage-1338: one mechanism).
Agreed with Gander (gander-0002, morgana-0003, gander-0005):
- One bus verifier,
verifyPrincipal(request, connection) -> {kind, business, id}, kindowner,userortester. It mints one transient principal per request. It returns no role. The broker reads the level from the live grant on every request, so a grant change acts without a new principal. - Gander recommends FED 3.6's shape for users (path-bound
user-cli.mjs, direct parent the bus tree'spackages/webui/src/cli.mjs serve) plus kernel peer PID binding, and weighs bus-side ID-token verification as the larger alternative. - Correction from Gander, accepted: the tester proof doesn't run the full agent walk, because agents bind
tester by design (Q173). Managed runs still fail it. The tester listener moves from TCP 7333 to a private
Unix socket, with
bus.testerunchanged (agreed by Sage with Gander).
What row 193 needs from row 196:
- H1. Owner on the CLI keeps today's proof. Owner in the Console is a
console.sso.subjectsentry. - H2. A user principal comes only from an OIDC sign-in, through a path the bus verifies. No CLI path for users.
- H3. The user binding fails closed on no live grant, an unknown subject, a subject also in
console.sso.subjects, or a missing or invalid access.json. A broken access.json never stops the owner. - H4. Agent processes never get an owner or user principal.
- H5. Revocation ends a bound user within 30 s and at once on
access.reload, from the same access state row 193's share check reads.
The same-uid limit. Within one instance, a non-agent process on the instance's uid can claim the owner, and can start its own Console copy and claim any granted user (FED 3.6). An unsandboxed same-uid agent that detaches from its ancestry can do the same. The walk doesn't stop that, and Gander's design won't claim it does (gander-0005). Several humans don't make this worse: a user principal is no stronger than the owner principal that the same process could already take. Users themselves are not on the uid. They reach the bus only through the Console, so the limit is about local processes, not other humans.
Does row 190 narrow it? Row 190 runs several instances under one uid, so no. Row 190b puts an instance (the hub) under its own Linux user (mos-0986). That narrows reach across instances: another instance's uid fails the uid check (human.mjs:67) and can't read the hub's store or access.json. It does not narrow reach within one instance. Private threads are private from other humans using the bus, not from processes running as the instance's uid.
7. Migration and rollback (brief point 6)
- No access.json: the business has exactly its owner, every verb behaves as today, and the share verbs
work for the owner only (target
*or none). A broken access.json revokes users and leaves the owner. - Existing threads: already owned by the business human. Nothing is rewritten.
- Schema: 3i to 3j adds
thread_sharesonly. A re-pin past a schema bump needs a--migratewindow with Jason, and row 185's backup before activate covers it. - Rollback before any share is written: activate the previous copy with the store restored from the pre-activate backup. A 3i copy refuses a 3j store, as it does for every bump.
- 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, 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, 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, includingthread.create,thread.open,thread.sendandthread.share. - Mixed grants: collaborator on project A and viewer on B. Create in A works, create in B refuses.
- 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 readsthread.messagesand getsthread-not-ownedonthread.sendandthread.open. Share with*reaches every covered user and the owner. Share with a user whose grant doesn't cover the project or instance refusesshare-not-covered. - A share never widens: the target's grant drops the project, the next read refuses.
- Revoke, expiry and an invalid file each end share reads on the next request. The 30 s timer and
access.reloadpaths each have a case. - A revoked thread owner hides the thread from sharees.
- Unshare ends access. Share, unshare, share again leaves it shared (latest row wins).
- Non-owner
thread.share,thread.unshare,thread.sharesrefuse. Tester refuses on all three. thread.listplain shape unchanged.thread.list {shared: true}returns only readable threads.- Board reads refuse users and viewers.
- Governance pin (B1, sage-1368): a user-thread run raises a decision; it shows in the owner's
decisions recentwith its question text, andinboxlists it when routed to the human; the run's role claim shows in the owner'sagents, its task write intasks, and itsrecord.writeinproject.file(B3, sage-1373); the user's message bodies stay hidden from the owner'strailandreplies. The test names each item of the 5.7 user sentence, so the sentence and the pin match. trail, for the owner, a reader session and the tester (193b1): a user's thread message assubjectfailsmessage-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 (193b1): a hiddenidfailsmessage-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_sharesempty and append-only (update, delete, replace each abort). Restore-guard andCOUNTEDinclude it.
Business (193a): R1 accepts viewer, refuses any other new level.
CLI (193c): mosaic access user add/remove and grant --level viewer refuse tester and agent callers,
write a receipt line and call access.reload.
Webui (193d): a viewer has no composer or share control. With each UI check removed, the bus still refuses.
Mutants, each must be killed:
#readablerule 1 dropped (any covered user reads any thread).- Rule 2 dropped (a share widens the grant).
- Rule 3 dropped (a revoked owner's thread stays visible).
- Latest-row lookup replaced by "any share row" (unshare ignored).
- (Moved to FED R4, N3: the level-loss mutant is R4's.)
- Viewer allowed
thread.send. #threadReadused forthread.send(sharee writes).share-not-coveredcheck removed.thread.shareowner check removed (sharee re-shares).- A user allowed
trail. - The
trailfilter removed (the owner reads a user's unshared message by id). - The
repliesfilter removed (the owner reads replies in a user's unshared thread).
9. Build rows and order
| 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, 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 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).
R1 exports one pure covers(grant, {business, project, instance}, now, {level}). Row 193 uses only that
function and adds no second one (Sage confirmed, sage-1354):
- share reads (5.4 rules 2 and 3) and
share-not-covered:covers(g, thread, now, {}), any level; thread.create,thread.open,thread.send,thread.share:covers(g, thread, now, {level: 'collaborator'});project.listandnamesfor users: any level, as FED 3.5.
covers needs no further argument for row 193. If r4.1 changes in review, 193b1 follows it.
Follow-ups, not in this row:
- A read-only users and grants page for the owner in the Console.
- A second owner or admin level, if Jason wants one.
- An explicit, logged owner audit verb for user threads, only if Jason asks for one (sage-1354).
10. Gate
Per the brief: Duckworth approves this design, then the manifest. The bus, business, cli and webui suites and
every scripts/test-*.sh pass on Sage's gate, and the section 8 mutants are killed. No test touches the live
directories. No product code before Duckworth approves.