From 60b9b112a161f1a51484d44f39888153156871e2 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 13:56:54 -0500 Subject: [PATCH] gizmoduck: T3 federation design r2 for #414: user proof bound to the Console tree outside agents (B1), named ownership changes (B2), one collaborator level with an exact verb list (B3), three revocation triggers and launcher refusal (B4), separate uid as R8's entry gate (B5) --- agents/gizmoduck/work/federation/DESIGN.md | 323 ++++++++++++++------- docs/SESSIONS.md | 1 + 2 files changed, 223 insertions(+), 101 deletions(-) diff --git a/agents/gizmoduck/work/federation/DESIGN.md b/agents/gizmoduck/work/federation/DESIGN.md index 0f4570d47..02ef23d7a 100644 --- a/agents/gizmoduck/work/federation/DESIGN.md +++ b/agents/gizmoduck/work/federation/DESIGN.md @@ -1,24 +1,44 @@ # T3 federation design: factory and hub (Vikunja #414, G3, G4, C1.1) -Author Gizmoduck (Opus 5.5), 2026-10-20, r1. Design only, no product code. Needs an independent review -(Filbert or Dewey) before any row goes to Sage's queue. Code lines are at origin/refactor 9ae8c73ac. The -Q175 SSO design is cited at Sage's local commit 817e8f51b (agents/sage/work/q175-sso/DESIGN.md), which -was not on origin when this was written. +Author Gizmoduck (Opus 5.5), 2026-10-20, r2. Design only, no product code. Reviewer Duckworth (Opus 5.5, +mos-0978), r1 verdict changes B1-B5 (duckworth-0001, REVIEW.md at b402197ff). R1, R2 and R5 go to Sage's +queue once Duckworth approves, and Sage assigns owners. Code lines are at origin/refactor 9ae8c73ac. The +Q175 SSO design is cited at Sage's local commit 817e8f51b (agents/sage/work/q175-sso/DESIGN.md), which was +not on origin when this was written. Binding inputs: TARGET.md addendum 2026-10-20, DECISIONS Q175 to Q178, projects/mosaic-mvp/GAP-MAP.md and PLAN.md T3 (all in jason-master-restart). +## Changes from r1 + +- B1. A user principal now comes only from the Console's verified OIDC session. The user CLI takes the + human proof's walk, which refuses agent markers and binaries, and its direct parent must be the + Console from the bus's own tree. This needs the path-bound human proof (P19 333). Residual risk in 3.6. +- B2. 3.6 names the ownership changes: `#owner`, `#threadFor`, the `#human` gates and `thread.send`'s + sender fields. `message.send` is on no list, and `#threadFor` refuses users as a second wall. +- B3. One level for 10-27, `collaborator`, with an exact verb list and a filter per read (3.5). Viewer and + every board read wait. +- B4. Revocation has three triggers: the next user request, a 30 s broker timer, and a reload verb that + R5 calls. The launcher refuses to start a revoked thread, because the broker reports it on every + `threadsState`. It also SIGTERMs a running one (3.7). R4 now depends on R3 and R5. +- B5. A separate uid per instance is a hard entry gate of R8, with checks (mos-0984 point 2). +- N1 to N9 are handled where they apply: v1 count (2), `configDir` at business.mjs:25-28 (5), R1 takes + providers and peers as arguments (7), "human-only" (3.5), file checks (3.5), `instances` as the real limit + (3.5), provider removal needs a Console restart (3.7), Q1 is with Jason (9), client ids in R8 (7). +- Mos's rulings in mos-0978 and mos-0984 replace the r1 questions (section 9). The push credential note is + in section 10. + ## 1. Summary By 10-27, federation means a person from one instance can sign in to the other instance's Console and use -exactly what its owner granted: a business, some projects, some agent instances, at a viewer or -collaborator level. The receiving instance verifies that person itself through OIDC (Q175) and its broker -enforces the grant on every request. Revoking a grant refuses the next request and stops that user's -thread runs. +exactly what its owner granted: one business, some projects and its thread instance, at the collaborator +level. The receiving instance verifies that person itself through OIDC (Q175) and its broker enforces the +grant on every request. Revoking a grant refuses the next request and stops that user's thread runs within +about 30 s, or at once when the grant CLI makes the change. Instance-to-instance traffic, where an agent on the factory talks to a role on the hub, waits until after -10-27. Section 6 sketches it (Ed25519 instance keys and signed HTTP requests) so the 10-27 slice doesn't -block it. +10-27 unless Jason rules otherwise (mos-0984 point 3). Section 6 sketches it (Ed25519 instance keys and +signed HTTP requests) so the 10-27 slice doesn't block it. ## 2. What exists today (measured) @@ -28,26 +48,36 @@ block it. - F2. A business has exactly one human: `human` at packages/business/src/business.mjs:160. `bindHuman` refuses any other name (packages/bus/src/broker.mjs:532-538). - F3. The human proof is a cooperative same-uid process check, not a wall. `verifyHuman` compares the - caller's uid, start time, nonce and CLI path (packages/bus/src/human.mjs:52-71). The tester proof works - the same way (packages/bus/src/tester.mjs:5-9). The server takes exactly one of `cap`, `human` or - `tester` per request (packages/bus/src/server.mjs:52-70). + caller's uid, start time, nonce and CLI path (packages/bus/src/human.mjs:52-72), then walks the parents + and refuses agent markers, launch records and agent binaries (:73-88). The tester proof refuses only + managed runs and allows agents by design (packages/bus/src/tester.mjs:5-9, :56-60). The server takes + exactly one of `cap`, `human` or `tester` per request (packages/bus/src/server.mjs:52-70). - F4. Authority for agents is checked in the broker: `#checkAuthority` and `#authorityFor` (broker.mjs:763-799), with classification at :596-609. Every one of these is about a role run. Humans get no roles. `#human` (broker.mjs:375-378) is the only human check. - F5. Threads have a single `owner` column (packages/bus/schema.sql:247). `#thread` refuses a thread whose owner isn't the caller (broker.mjs:356-362). `#owner` returns `tester` or the human (broker.mjs:364-366). - `#threadHolders(business, instance, owner)` already finds the held runs of one owner's threads - (broker.mjs:298-306). + `#threadFor` special-cases the human (:326) and checks reply chains for the tester only (`#testerOwns`, + :352-355, called at :337 and :342). `#threadHolders(business, instance, owner)` finds the held runs of + one owner's threads (broker.mjs:298-306). - F6. The Console reaches the bus as the one human by spawning `human-cli.mjs` per call - (packages/webui/src/bus.mjs:1-9, 27-31). Its sign-in today trusts a proxy header (Sage's design, section - "What exists today"). Q175 replaces that with an OIDC relying party that takes a `subjects` list from a - `console.sso` key in config.json (Sage's design sections 1 and 4). + (packages/webui/src/bus.mjs:1-9, 27-31). Its entry is `packages/webui/src/cli.mjs serve` + (scripts/webui-service.sh:3). Its sign-in today trusts a proxy header (Sage's design, section "What exists + today"). Q175 replaces that with an OIDC relying party that takes a `subjects` list from a `console.sso` + key in config.json (Sage's design sections 1 and 4). - F7. config.json is read by one loader that refuses unknown top-level keys (scripts/mosaic-config.mjs:112-116). The business file is writable by agents through `organizationWriter` (packages/business/src/organization.mjs:83), which is why Sage put SSO config in config.json and not the business file. - F8. Bus, Discord and Console run as `systemctl --user` units (scripts/bus-service.sh:200), so every instance on a host runs under Jason's uid today. +- F9. Board reads are per business, not per project or instance: `inbox`, `agents`, `tasks`, `trail` at + broker.mjs:2143-2146, `decisions` :2174, `credentials` :2195, `messages` :2217 (the Console's set is + views.mjs:3-13). `project.list` returns every project in the business (:2318-2322). +- F10. The launcher restarts a thread that has pending messages and no holder (packages/cli/src/launcher.mjs:533), + launched as the business human (:542), every 2 s (:133). Its `startedFor` guard is in memory. A claim end + stops a run only at its next bus call (broker.mjs:2273-2275). The idle stop is the one place that + SIGTERMs a thread runner (launcher.mjs:527). ### The archived v1 federation (v1/apps/gateway/src/federation) @@ -59,8 +89,8 @@ and re-checks the grant row on every request. Scopes are zod-validated resource Verdict: none of the code is reusable here. -1. It is about 4,100 lines of NestJS, Drizzle and Postgres code (`wc -l`), plus a step-ca dependency. The - new stack has none of those. +1. It is about 4,470 lines of NestJS, Drizzle and Postgres code without tests (9,198 with them), plus a + step-ca dependency. The new stack has none of those. 2. Inbound federation never worked. The guard depends on `requestCert: true`, but the server is built as `new FastifyAdapter({ bodyLimit })` with no TLS options (v1/apps/gateway/src/main.ts:50-53). Every inbound call would get 401. @@ -77,8 +107,8 @@ enrollment short-lived and single-use. Tag every federated result with its sourc 1. D1. Each instance names itself and its peers in config.json, so the Console can show "you are on the hub" and link to the factory. Peer entries carry no trust in the 10-27 slice. They are labels and URLs. -2. D2. The owner of an instance grants a person access to one business, with a project list, an agent - instance list and a level. Grants live in a human-authored access file on the receiving instance. +2. D2. The owner of an instance grants a person access to one business, with a project list and an + instance list. Grants live in a human-authored access file on the receiving instance. 3. D3. That person signs in to the receiving instance's Console with OIDC. The Console verifies the token itself (Q175) and matches `(provider, sub)` to an access-file user with a live grant. 4. D4. The broker, not the Console, enforces the grant on every request: which verbs, which projects, @@ -89,20 +119,22 @@ enrollment short-lived and single-use. Tag every federated result with its sourc ### 3.2 What waits - W1. Agent-to-agent traffic between instances (section 6). -- W2. Shared threads between several users (C1.4). That is T1.3's. A granted user gets private threads - only. +- W2. Shared threads between several users (C1.4), and a viewer level. Both are T1.3's. A granted user gets + private threads only. - W3. Remote owners. A grant never gives owner verbs: organization, instances, routes, plugins, limits, decision resolution, launch revoke and the kill switch stay with the local human. - W4. A registry, `mosaic comms`, enrollment tokens and key rotation tooling. With two instances, out-of-band setup by Jason is cheaper. -- W5. Federated reads that merge results from both instances into one view. +- W5. Federated reads that merge results from both instances into one view, and any board read for a + granted user (F9). ### 3.3 Instance identity in the slice An instance is its public HTTPS origin plus its own OIDC client at the provider. The factory and the hub are two clients, so their ID tokens carry different `aud` values. The Q175 audience check refuses a -factory token presented at the hub, which makes tokens instance-bound by signature, not by a header. No -instance key is needed until instances talk to each other (W1). +factory token presented at the hub, which makes tokens instance-bound by signature, not by a header. This +holds only if the two client ids differ, which R8 checks. No instance key is needed until instances talk +to each other (W1). ### 3.4 Remote user identity @@ -115,10 +147,10 @@ home instance's word for who the user is. The owner path stays as Sage designed it: `console.sso` subjects sign in as the business human. Everyone else comes from the access file. The two lists are disjoint, and the loader refuses a subject in both. -### 3.5 The access file +### 3.5 The access file and the collaborator level -`/access.json`, next to config.json, so it is per instance (business.mjs:25-27 sets configDir -from `MOSAIC_CONFIG`). It is not in config.json for two reasons. Revocation has to take effect without a +`/access.json`, next to config.json, so it is per instance (`configDir` at business.mjs:25-28 +reads `MOSAIC_CONFIG`). It is not in config.json for two reasons. Revocation has to take effect without a restart, and a typo in config.json stops every tool on the instance (Sage's design, section 4). It is not in the business file because agents can write that (F7). @@ -135,7 +167,7 @@ in the business file because agents can write that (F7). "user": "jason", "business": "creative-hub", "projects": ["*"], - "instances": ["researcher", "fact-check"], + "instances": ["researcher"], "level": "collaborator", "expires": "2026-12-31T00:00:00Z" } @@ -143,57 +175,132 @@ in the business file because agents can write that (F7). } ``` -Rules the loader enforces, refusing the whole file on any failure (fail closed): +Rules the loader enforces, refusing the whole file on any failure (fail closed). The loader takes the +provider ids, peer ids and declared businesses as arguments, so it stays pure: +- The file is a regular file, not a symlink, owned by the instance's uid, with no group or other write. + These are the checks Sage's loader runs on the secret slot. - Unknown keys refuse. `accessVersion` is 1. `revision` is a positive integer. -- A user id matches the business id rule and is never `human`, `tester`, or any business's `human`. That - keeps a granted user from owning threads as the owner or the tester (F5). -- `subjects[].provider` names a provider in `console.sso`. A `(provider, sub)` pair appears once in the - file and never in `console.sso.subjects`. -- `home` names a peer in `federation.peers`, or is `local` for a non-federated extra user. +- A user id matches the business id rule and is never `human`, `tester`, or any business's `human`. +- `subjects[].provider` names a given provider. A `(provider, sub)` pair appears once in the file and + never in `console.sso.subjects`. +- `home` names a given peer, or is `local` for a non-federated extra user. - A grant names a declared business. `projects` is `["*"]` or declared project ids. `instances` is - `["*"]` or declared instances. `level` is `viewer` or `collaborator`. `expires` is optional ISO time. + `["*"]` or declared instances. `level` is `collaborator`, the only level in 10-27. `expires` is optional + ISO time. - A grant past `expires` is treated as absent. +- A file that fails any rule leaves every user with no grant, which revokes everything. That is the safe + direction. -Levels, as templated human roles. A grant can only narrow them through `projects` and `instances`: +The collaborator level is an exact verb list. Every other verb fails `access-refused` before the verb's own +checks. That covers all owner verbs (W3), every board read (F9), `message.*`, `decision.*`, project files, +`spec.read`, `assistant.stop`, `launch.*`, `name.set` and `usage.*`. -| Level | Allowed | -|---|---| -| viewer | Read the board, projects and instances it is granted. Read its own threads. No sends. | -| collaborator | Viewer, plus create its own threads in granted projects on granted instances, send in them, read replies. | +| Verb | Today | For a user | +|---|---|---| +| `project.list` | every project in the business (:2318-2322) | granted projects only | +| `thread.create` | the business's one thread instance (:2331-2334) | project granted, and that instance in the grant's `instances`, else `access-refused` | +| `thread.list` | owner filter (:2351-2362) | owner `user:`, and only threads whose project and instance are still granted | +| `thread.open` | `#thread` owner check (:2365-2371) | same, plus the thread's project and instance still granted | +| `thread.send` | `#thread`, sender `human`, writes `human.input` (:2374-2398) | same grant check. Sender `from_role` `user`, `from_run` `user:`, no `human.input` | +| `thread.messages` | `#thread` (:2401-2420) | same grant check | +| `names` | business-wide (:2288-2295) | granted instances only | -Writes go through a human-only CLI verb (row R5), atomically with a revision bump and one receipt line -in `/state/access-log.jsonl` (append-only, as `activation-log.jsonl` is). Jason may also edit the -file by hand. The next read refuses a bad edit, which revokes everything, the safe direction. +`human.input` events are the owner's authority evidence (the `input` view, broker.mjs:1203-1207). A user's +message must never write one, the same rule the tester follows at :2398. -### 3.6 Enforcement in the broker +`instances` is the real limit of a grant. The thread instance runs with its full role authority and is +launched as the business human (F10), so a collaborator's message drives that instance with the owner's +role. A grant gives a user exactly what the granted instance can do. The grant docs and the R5 help text +say so. -A new bus principal, the user. It binds through a new path-bound CLI, `packages/bus/src/user-cli.mjs`, -proven the way `tester-cli.mjs` is (F3), carrying `{ business, user }`. The broker: +Writes go through `mosaic access grant` and `revoke` (row R5), atomically with a revision bump and one +receipt line in `/state/access-log.jsonl` (append-only, as `activation-log.jsonl` is). The +CLI checks its caller with the human proof, but a direct file write by the instance's uid skips both the +CLI and the receipt. Only the separate uid (R8 gate) keeps other instances' agents from making that +write. Jason may also edit the file by hand. -1. Reads the access file through a reader that re-reads on change, the way `settings.current(id)` does - for instance settings (packages/bus/src/runtime.mjs:140). -2. On bind and on every request, finds the user's live grant for the business, or fails `access-revoked`. -3. Allows only the verbs on the level's list. Every other verb fails `access-refused`, including all owner - verbs (W3). -4. Checks project and instance arguments against the grant before the verb's own checks. -5. Uses `user:` as the thread owner, so `#thread` (broker.mjs:356-362) keeps users apart from each - other, from the human and from the tester with no new ownership code. -6. Records `action.refused` for each refusal, as `refuse()` does today (broker.mjs:526-530). +### 3.6 The user principal in the broker -The Console only authenticates. It picks the transport by session: the owner session keeps -`human-cli.mjs`, a granted user's session uses `user-cli.mjs`. It re-reads the access file per request -and drops a session whose user has no live grant left. The bus check stays authoritative, so a Console -bug can't widen a grant. +The bar (B1, mos-0984 point 1): a bus user principal comes only from something the bus can verify. Here +that is the Console's verified OIDC session, reached through a proof the bus checks itself. The calling +process's claim alone isn't enough, and neither is a proof that admits agent ancestry. + +The proof. A new path-bound CLI, `packages/bus/src/user-cli.mjs`, with its own nonce variable +(`MOSAIC_BUS_USER_NONCE`), so neither of the other CLIs' nonces proves it. The bus checks it with a +`verifyUser` that: + +1. Runs `verifyHuman`'s checks with `cliPath` set to the bus's own `user-cli.mjs`: same uid as the bus, + start time, nonce, CLI path, and the parent walk that refuses agent markers, launch records and agent + binaries (human.mjs:64-96). +2. Also requires the user CLI's direct parent to be the Console from the bus's own tree: `argv[1]` equals + the bus's resolved `packages/webui/src/cli.mjs`, and `argv[2]` is `serve`. This binds the user CLI to + the Console's tree, as #225 binds `human-cli` to its literal path. +3. Takes `user` from the proof and binds it only if the access file gives that user a live grant in the + proof's business. Otherwise `access-revoked`. + +So the claim "I am user jason" comes from a process the bus has verified as its own Console, outside any +agent. The Console makes that claim only after Q175 verified the ID token's signature, issuer, audience, +expiry and nonce and mapped `(provider, sub)` to the user. + +This needs the path-bound human proof, P19 333. Residual risk, stated as Sage's design section 8 states it +for the owner. The proof is a cooperative same-uid check. A non-agent process on the instance's uid, a +shell Jason opens for example, can start its own copy of the Console's tree from the bus's path and claim +any granted user. So can anything that can ptrace or write files as that uid. Agents can't, because the +walk refuses them. Other instances can't, because the uid check refuses them (R8 gate). Federation adds no +weaker path than the owner's. + +The broker changes, each with an R3 test: + +1. `bindUser({ business, user })` makes a session with `s.user` set and `s.human` and `s.tester` unset. +2. `#owner(s)` (:364-366) returns `user:` for a user session. Today it would return undefined. +3. `#threadFor` (:318-345) refuses a user session with `thread-required-use-thread-send`, as it refuses + the human at :326. `message.send` is also off the verb list. Two walls, so a reply-chain send into + another user's thread fails even if one is removed (R3 mutant). +4. The `if (!s.tester) this.#human(s)` gates on the seven verbs above become a check that admits the human, + the tester, or a user with a live grant. +5. Every other `s.human` and `s.tester` branch (48 and 41 in broker.mjs) gets an audit line in R3's + packet: user refused, user treated as human, or not reachable for a user. Since every non-list verb + fails first, most are "not reachable", and the audit shows it. +6. `refuse()` records `action.refused` for every refusal (broker.mjs:526-530), with `by` = `user:`. + +The Console only authenticates. The owner session keeps `human-cli.mjs`, a granted user's session uses +`user-cli.mjs` with the user id. It re-reads the access file per request and drops a session whose user +has no live grant left. The bus check stays authoritative, so a Console bug can't widen a grant. ### 3.7 Revocation -- Removing or expiring a grant: the next Console request drops the session, and the next bus request - fails `access-revoked`. Both re-read the file, so there is no cache to flush. -- On a revision where a user loses a grant, the broker ends that user's held thread claims in that - business using `#threadHolders(business, instance, 'user:')` (F5) and the existing end path. -- Removing a provider from `console.sso` refuses every user of that provider at sign-in. Live sessions - hold the provider id and are dropped on the next request when it no longer resolves. +A grant is lost when it is removed, when it expires, or when the file fails validation. Three triggers +find the loss: + +1. The next user request. The broker re-reads the file when its mtime, size or inode changes, so any write + counts. +2. A broker timer every 30 s re-reads and re-evaluates expiry. This catches hand edits and expiry with + no request at all. +3. `access.reload`, a new human-only verb. `mosaic access grant` and `revoke` (R5) call it after the + write, so CLI changes act at once. + +On a loss, in one transaction, the broker ends the claims of every `user:` thread that is no longer +covered (via `#threadHolders(business, instance, 'user:')`, F5, and `#claimEnd`). The run's next bus +call is then refused (F10). + +The launcher side, so a pending message can't start a new run: + +- `threadsState` (broker.mjs:390-393) adds `revoked: true` to any thread owned by `user:` that no live + grant covers. The broker computes this from the file on each call, so it survives a launcher restart, + unlike `startedFor`. +- The launcher's thread pass (launcher.mjs:510-553) never starts a revoked thread. +- For a revoked thread that is still running, the launcher SIGTERMs the runner at once, as the idle stop + does at :527, but without waiting for `threadIdle`'s release, since the claim is already ended. The + launch check also refuses a revoked thread, which covers a start racing the revocation. + +Bound: the bus refuses the user and the run from the next request on. The runner gets SIGTERM within one +launcher tick (2 s, launcher.mjs:133) of the broker seeing the loss. A model turn already in flight ends +when its runner exits. Its last bus call is refused, so it can't deliver a reply into the thread. + +Removing a provider from `console.sso` is different. config.json is read at start, so that takes effect +only after a Console restart. Removing the users from access.json acts at once, so the R5 help text says to +do that first. ## 4. Fit with the current code @@ -201,6 +308,8 @@ bug can't widen a grant. is the one network-facing piece. No row opens a bus port. - The broker's agent authority path (broker.mjs:763-799) is untouched. User checks are a separate path, next to `#human`, because humans have no role claim and `#agent` would refuse them (broker.mjs:367-374). +- New ownership code is small but real: `#owner`, `#threadFor`, the seven verb gates, `thread.send`'s + sender fields and `threadsState` (3.6, 3.7). - The business file keeps one `human`. Granted users sit outside it, which keeps every existing business-file test and the organization writer unchanged. - The config loader gains one optional key, `federation`, in the same file Sage's phase 1 changes @@ -216,24 +325,28 @@ bug can't widen a grant. ## 5. Dependencies on T1 -- T1.1, several instances per host. Needed for the live two-instance acceptance, not for building the - rows. The rows and the scratch acceptance (R7) use separate `MOSAIC_CONFIG` files and data roots, which - the code already honours (business.mjs:24). Live needs two Consoles, two public origins, two Traefik - routes and two provider clients, which are Jason-gated infra. -- T1.1 and the uid. Both instances run under one uid today (F8), and the human, tester and user proofs are - same-uid cooperative checks (F3). An agent on the factory can therefore write the hub's access.json or - run the hub's CLIs. Grants are only as strong as that boundary. I recommend T1.1 runs each instance - under its own uid with its config dir and data root mode 0700. Without that, the hub's isolation from - factory agents is policy, not mechanism. +- T1.1, several instances per host, with a separate uid per instance (row 190, Darkwing). Rows R1 to R7 + build and test without it: they use separate `MOSAIC_CONFIG` files and data roots, which the code already + honours (business.mjs:25-28). R8 refuses to start without it (mos-0984 point 2). With per-uid instances, + a factory-uid process can't read or write the hub's access.json or data root, and it can't pass the + hub's human or user proof, because `verifyHuman` requires the caller's uid to equal the bus's + (human.mjs:67). Inside one instance the proofs stay same-uid cooperative (3.6, P19 333). + If row 190 can't deliver separate uids, Mos takes a new decision to Jason then. The design doesn't plan a + single-uid live run. +- T1.1 live also needs two Consoles, two public origins, two Traefik routes and two provider clients, + which are Jason-gated infra. - T1.2, #412 SSO. Hard dependency. Row R6 builds on Sage's phase 1 candidate (Q177 merge-chaining) and goes live only after the phase 2 window. -- T1.3, human users and RBAC. The access file is a user and grant store, which T1.3 also needs. One store - should serve both. I propose this file as the shared shape: T1.3 adds shared threads and more levels to - it, federation adds `home`. If T1.3 picks a different store, R1 and R3 rebase onto it. +- T1.3, human users and RBAC. Mos's reading (mos-0978 Q2) is one user and grant model for local and + federated humans. Row 193 (Morgana, with Sage) decides the store. This design proposes access.json as + that shared shape: T1.3 adds shared threads and more levels, federation adds `home`. If row 193 picks + another store, R1 becomes an adapter or is dropped, and R3 and R5 rebase onto it. The broker check in + 3.6 and the revocation in 3.7 don't depend on where the grants are stored. ## 6. After 10-27: instance-to-instance -Sketch only, so the slice doesn't block it. +Sketch only, so the slice doesn't block it. If Jason puts agent-to-agent inside 10-27 (mos-0984 point 3), +rows for it are added after r2. - Each instance gets an Ed25519 key in a secrets slot (`~/.config/mosaic-dev/secrets//`, 0600, per the Q173 slot rule). Peers pin each other's public keys in `federation.peers[].keys`. Public keys are @@ -254,31 +367,39 @@ gate. Chains follow Q177: a dependent row builds on its parent's candidate. | Row | Work | Owner model | Depends on | Test gate | |---|---|---|---|---| -| R1 | `packages/business/src/access.mjs`: access.json loader and validator, pure, no I/O beyond one read | Codex Sol | none | business tests: every rule in 3.5 refuses with the key path named, expiry, `*`, disjoint from `console.sso.subjects`, `human`/`tester` ids refused | +| R1 | `packages/business/src/access.mjs`: access.json loader and validator. Providers, peers and businesses come in as arguments. One read plus the file checks | Codex Sol | none | business tests: every rule in 3.5 refuses with the key path named, symlink, non-regular file, other owner, group or other write, expiry, `*`, disjoint from the given subjects, `human`/`tester` ids, a level other than `collaborator` | | R2 | `federation` key in scripts/mosaic-config.mjs | Haiku 5.5 high | Sage #412 phase 1 candidate | test-config.sh: valid key loads, unknown sub-key and bad origin refuse, config without it loads | -| R3 | Broker user principal: `bindUser`, `user-cli.mjs` proof, level verb lists, project and instance checks, `user:` thread owner, per-request grant read | Opus 5.5 | R1 | bus tests: each level's allowed and refused verbs, every owner verb refused, other user's thread refused, revoked and expired grant refused on next request, refusals recorded. Mutants: skip the grant read, drop the project check, accept a user named like the human | -| R4 | Revocation ends the user's thread runs | Codex Sol | R3 | bus test: held thread claims of the user end on the revision that drops the grant, another user's runs stay | -| R5 | `mosaic access list, grant, revoke`: human-only, atomic write, revision bump, receipt line | Haiku 5.5 high | R1 | cli tests: tester and agent callers refused, concurrent write refuses on revision conflict, receipt appended, bad grant refused before write | +| R5 | `mosaic access list, grant, revoke`: human proof, atomic write, revision bump, receipt line. Help text states the `instances` limit and the provider-removal order | Haiku 5.5 high | R1 | cli tests: tester and agent callers refused, concurrent write refuses on revision conflict, receipt appended, bad grant refused before write | +| R3 | Broker user principal: `verifyUser` and `user-cli.mjs`, `bindUser`, the 3.6 changes, the collaborator verb list and its filters, per-request grant read, audit lines for every `s.human`/`s.tester` branch | Opus 5.5 | R1 | bus tests: each listed verb allowed, every other verb refused, a leak test per listed read (other projects, other instances, other users' threads), a reply-chain `message.send` into another user's thread refused, `thread.send` writes no `human.input`, revoked and expired grant refused on next request, refusals recorded. Mutants: the user CLI accepted under an agent-marked parent, accepted with a parent that isn't the Console, the grant read skipped, a read filter dropped, `#threadFor`'s user refusal removed, a user named like the human accepted | +| R4 | Revocation: 30 s timer, `access.reload` verb, claim end on loss, `threadsState` `revoked`, launcher skip and SIGTERM, launch check refusal, R5 calls `access.reload` | Codex Sol | R3, R5 | bus and launcher tests, each with a mutant: a hand edit removing the grant ends the run within one timer period, an expiry with no request ends it, a pending message followed by a launcher restart starts no run, a running revoked thread gets SIGTERM, another user's runs stay | | R6 | Console: granted-user sign-in, `user-cli` transport per session, per-request grant re-read, owner-only controls hidden, peer links | Opus 5.5 | Sage #412 phase 1, R1, R2, R3 | webui tests on Sage's fake OP: granted user signs in, ungranted `sub` refused, revoked mid-session gets 401 on next call, a granted user's owner-verb call refused by the bus even with the UI check removed (mutant) | -| R7 | Scratch two-instance acceptance: two configs, two data roots, two Consoles, one fake OP | Haiku 5.5 high | R1-R6 | script test: Jason granted collaborator on one hub project signs in to the hub, makes a thread, sends to a granted instance, is refused on another project and instance, is revoked and gets refused with the run ended. The factory shows nothing of the hub | -| R8 | Live window, not a code row: hub provider client, Traefik route, access.json written by Jason, re-pins | Jason, Mos | T1.1 live, #412 phase 2, R7 | Jason signs in to the hub from his account, sends one message, Melanie revokes, his next request is refused | +| R7 | Scratch two-instance acceptance: two configs, two data roots, two Consoles, one fake OP. The Consoles start outside agent ancestry (a transient user unit), as the user proof requires. They never use the live stack's units or ports | Haiku 5.5 high | R1-R6 | script test: Jason granted collaborator on one hub project signs in to the hub, makes a thread, sends to the granted instance, is refused on another project and on board reads, is revoked and gets refused with the run ended. The factory shows nothing of the hub | +| R8 | Live window, not a code row: hub provider client, Traefik route, access.json written by Jason, re-pins | Jason, Mos | T1.1 live with separate uids, #412 phase 2, R7 | Entry gate, R8 refuses to start unless all hold: the factory and hub buses run under different uids, a factory-uid human-cli and user-cli call to the hub socket is refused (human.mjs:67), the factory uid can't read the hub data root or config dir (both 0700), and the two provider client ids differ. Then Jason signs in to the hub from his account and sends one message, Melanie revokes, and his next request is refused | -R1, R2 and R5 can start now and run in parallel. R3 starts when R1 has a candidate. R6 waits on Sage's -phase 1 candidate. +R1, R2 and R5 can start now and run in parallel. R3 starts when R1 has a candidate, R4 when R3 and R5 do. +R6 waits on Sage's phase 1 candidate. ## 8. Risk to 10-27 -The slice lands only if #412 phase 1 has a reviewed candidate by about 10-23 and T1.1 is live by about -10-25. R6 and R7 chain on the first, R8 needs both plus two Jason-gated windows (#412 phase 2 and the -federation re-pin) and infra work only Jason does: a second public origin, a Traefik route and a provider -client. The one-uid layout (F8) also means grants don't isolate the hub from factory agents until T1.1 -separates uids. +The slice lands only if #412 phase 1 has a reviewed candidate by about 10-23 and T1.1 with separate uids is +live by about 10-25. R6 and R7 chain on the first. R8 needs both, plus two Jason-gated windows (#412 phase +2 and the federation re-pin), plus infra work only Jason does: a second public origin, a Traefik route and +a provider client. If row 190 can't deliver separate uids, R8 doesn't run and Mos takes it to Jason. -## 9. Questions for Mos +## 9. Rulings (mos-0978, mos-0984) -1. Q1. Is human cross-access enough for 10-27, with agent-to-agent traffic after (section 6)? I recommend - yes. Agent traffic needs instance keys, a signed endpoint and a peer grant kind, which is at least three - more rows on top of a chain that already sits on #412. -2. Q2. Does T1.3 take the access file as its user and grant store (section 5)? One store avoids two RBAC - models. This needs Sage's agreement as T1 lead. -3. Q3. Does T1.1 run each instance under its own uid? Without it the hub's grants are policy only. +1. Q1: human cross-access is the 10-27 slice (mos-0978). Duckworth's N8 says moving agent-to-agent past + 10-27 narrows Jason's "Federation is configurable", so Mos is putting it to Jason (mos-0984 point 3). + r2 doesn't wait for that answer. If he puts it inside 10-27, rows follow. +2. Q2, open: one user and grant model for local and federated humans is Mos's reading. Row 193 (Morgana, + T1.3) settles the store with Sage. +3. Q3 and B5: a separate uid per instance is a hard precondition of R8 (mos-0984 point 2). Mos won't ask + Jason to accept a single uid, and takes a new decision to him if row 190 can't deliver. + +## 10. Notes + +- The r1 push used the repo-local credential helper `scripts/git-credential-mosaic.mjs` (set in the + canonical checkout's .git/config for git.mosaicstack.dev) with its default credential file, + `~/secrets/mosaic.gitea.json`, and no `MOSAIC_GITEA_CREDENTIAL_FILE` override. That file's user is + jason.woltje. The r2 push uses the same. Gizmoduck has no seat identity until T1 row 195 adds per-agent + git identity. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 23e8b8f87..2c8bc6a53 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1358,3 +1358,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-20T15:40:36Z | Filbert (Opus 5.5, 048f3c63) | row 163 r2 review (D2 rehearse, candidate 2c090eefd, manifest c6f4f4c12596) | approve, comment 29447 rev 2234: 249/249 singles pass; my M1-M6 and N1 mutants all killed (R14, R9, R11, R4, R15, session test) 2026-10-20T15:42:00Z | Sage (Opus 5.5, 1ef1e4f8) | row 163 (#1660) r2 verdict intake | approved by Filbert (comment 29447, rev 2234 committed cd2b377bb); P19 337 comment 367 -> 368; candidate 2c090eefd manifest c6f4f4c12596 waits to land after B7, fresh gate if the base moves 2026-10-20T18:44:50Z | Gizmoduck (Opus 5.5) | T3 federation design r1 for Vikunja #414 (G3, G4, C1.1), agents/gizmoduck/work/federation/DESIGN.md, on branch gizmoduck/federation-design off origin/refactor 9ae8c73ac | design committed and pushed; rows R1-R8 wait for Filbert or Dewey review before Sage's queue; Q1-Q3 to Mos +2026-10-20T18:56:54Z | Gizmoduck (Opus 5.5) | T3 federation design r2 for Vikunja #414, answering Duckworth's B1-B5 and N1-N9 (duckworth-0001) and Mos's mos-0978 and mos-0984 rulings, agents/gizmoduck/work/federation/DESIGN.md | r2 committed and pushed on gizmoduck/federation-design; sent to Duckworth and Mos for re-review; R1, R2, R5 wait for Duckworth's approval before Sage's queue