Files

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 the level of 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_shares needs schema 3j and a --migrate window with Jason.

2. What exists today (measured at 53bb59462)

  • One human per business (packages/business/src/business.mjs:160). bindHuman refuses any other name (packages/bus/src/broker.mjs:532-538).
  • The owner's CLI proof is path-bound (#225): verifyHuman checks uid, start time, nonce (MOSAIC_BUS_CLI_NONCE), argv[1] equal to the tree's human-cli.mjs, then walks parents and refuses agent markers, launch records and agent binaries (packages/bus/src/human.mjs:64-96). authenticateHuman binds the proof to businesses[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) returns tester or s.human (broker.mjs:364-366). #thread refuses a non-owner with thread-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, writes human.input for 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). trail and replies return 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 in console.sso.subjects (FED 3.5). So one subject maps to at most one human.
  • A user id can't be human, tester or 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.list keeps its response shape exactly. Shared threads come only from thread.list {shared: true}, whose rows add owner. 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.unshare and thread.shares fail tester-refused, and a share target is never tester. Agent testers (Q173) test sharing through the bus suites, not the live 7333 listener.
  • reader sessions (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> and user remove <id>;
  • --level viewer on grant;
  • the receipt line in <dataRoot>/state/access-log.jsonl and the access.reload call, 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).
  • 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.
  • No existing row changes. Existing threads are already owned by the business human (#owner at 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. to is user:<id>, the business human's name or *. A user:<id> target must have a live grant, of any level, covering the thread's project and instance, else share-not-covered. Sharing with yourself fails invalid-request. A share that is already in force fails share-exists. Writes one share row.
  • thread.unshare {thread, to}: the caller must own the thread. The share must be in force, else share-not-found. Writes one unshare row. 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 records action.refused with by set (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:

  1. a share for t is in force whose target is the caller's owner string, or *;
  2. the caller is the owner, or the caller has a live grant of any level covering t.project and t.instance;
  3. if t.owner is a user, that user still has a live grant covering t (a revoked owner hides the thread, sage-1338);
  4. t.owner is not tester.

#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 #readable against the access state the broker already refreshes on each request (mtime, size, inode), on its 30 s timer, and on access.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, 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)

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}, kind owner, user or tester. 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's packages/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.tester unchanged (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.subjects entry.
  • 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_shares only. A re-pin past a schema bump needs a --migrate window 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, 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: 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.
  • 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.reload paths 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.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.
  • 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; the run's role claim shows in the owner's agents, its task write in tasks, and its record.write in project.file (B3, sage-1373); the user's message bodies stay hidden from the owner's trail and replies. 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 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 (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.

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:

  1. #readable rule 1 dropped (any covered user reads any thread).
  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. (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.
  9. thread.share owner check removed (sharee re-shares).
  10. A user allowed trail.
  11. The trail filter removed (the owner reads a user's unshared message by id).
  12. The replies filter 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.list and names for 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.