Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2d161e2e6 | ||
|
|
edc794933e | ||
|
|
76f4605e19 | ||
|
|
19c015741d | ||
|
|
5d15979e06 |
@@ -19,9 +19,6 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
|
||||
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
|
||||
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
|
||||
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
|
||||
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Bushroot
|
||||
|
||||
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
|
||||
that retires the control board writer on 7331 (#1696).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,292 +0,0 @@
|
||||
# Row 198 design: retire the control board writer on 7331
|
||||
|
||||
Gitea #1696. Brief `docs/plans/2026-10-20_retire-control-board.md` (blob 11977a5cb, commit a31f1a0f8, Mos's
|
||||
call mos-1019). Owner Bushroot, reviewer Webby, gate owner Sage. Inventory: `agents/bushroot/work/queue-198/INVENTORY.md`
|
||||
(004f133bd, sha256 76b630ce...877).
|
||||
|
||||
Base: brief commit a31f1a0f8 is on origin `refactor` (head aa1bd209e). This branch rebases on `refactor` after
|
||||
row 192 lands (Sage's sequencing, ruling 4).
|
||||
|
||||
## Revision 5 (Webby REVIEW-r4.md, origin/webby/reviews 5a02ee395, on rev 4 9af75dc72; Mos mos-1045)
|
||||
|
||||
- **B1 (r4, the `--tester-port` swap)**: reverted. `parses_flag` is for `--board` only. The `--tester-port` grep at
|
||||
`stack-release.sh:670` stays. The `--tester-port` test row I added in rev 4 is removed.
|
||||
- **N1 (r4, stale mark)**: fixed. A write counter guards `index()` coalescing. `markSeen()` and `reply()` start a fresh
|
||||
scan and never join one. The new `board.test.mjs` case is in the new-tests table, and mutant M15 is in the table.
|
||||
- **Sequencing (sage-1386)**: row 197 lands before 198. The rebase is noted under Open points.
|
||||
|
||||
## Revision 4 (Webby REVIEW-r3.md, origin/webby/reviews c9d65664a, on rev 3 7e2ead7f3)
|
||||
|
||||
- **B1** (blocking child calls): fixed. The board's child calls become async. `createBoard` takes an async `exec`,
|
||||
and `index()`, `markSeen()` and `reply()` return promises. The sync `scan` and `replyToRow` stay for the CLI and
|
||||
their existing tests. See decision 10, the new B1 test and mutant M14.
|
||||
- **N1** (fallback copy): fixed. Decision 8 now says which copy gets the 7331 fallback.
|
||||
- **N2** (flag check matched a refusal): fixed. The parse check matches the parser branch, not the flag name. See
|
||||
decision 8. M12's target now names `--board` only in a refusal.
|
||||
|
||||
## Revision 3 (Sage rulings sage-1372, on rev 2 80fd45521)
|
||||
|
||||
- Ruling 1: `/api/seen` goes under `--send`, the same as `/api/reply`. Mos agrees (mos-1023 item 5). Both POSTs sit
|
||||
above `serve.mjs:558` (decision 5).
|
||||
- Ruling 2: the board context keeps the writer's read-only fleet default (decision 9).
|
||||
- Ruling 3: a rollback to a copy with no recorded `--board` falls back to 7331, which is dark, as it is today (decision 8).
|
||||
- Ruling 5: loopback stays read-only. No new gate in this row. The cross-uid loopback gate is row 196's. Sage took
|
||||
this to Mos (sage-1370). If Mos rules otherwise, Sage says so and this design changes.
|
||||
- Ruling 6, correction: neither the live pointer nor the live unit carries `--board`. Sage checked the live
|
||||
`ExecStart`: `cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com
|
||||
--auth-header X-authentik-username --allow-user jason.woltje --send --tester-port 7333`. Rev 2 said otherwise, and
|
||||
that was wrong. Decisions 6 and 8 are rewritten. The B2 guard stays. `webui-service.sh` lines 44, 71, 110 and 163
|
||||
still take and emit `--board`, so they are in the retirement.
|
||||
- Ruling 7: base. Brief commit a31f1a0f8 is on origin `refactor` at aa1bd209e.
|
||||
|
||||
## Revision 2 (Webby REVIEW.md, origin/webby/reviews 9afb947c7)
|
||||
|
||||
Rev 1 was c8fe23a37. Rev 2 answered B1-B3 and N1-N5 (80fd45521). Changes are marked where they land.
|
||||
|
||||
- **B1** (pinned copy): fixed. `packages/control-board/src` and `tools/tmux/agent-send.sh` join `COPY_PATHS`. The
|
||||
copy test starts the Console from a fresh copy and expects 200 on `/api/board`. See decision 7.
|
||||
- **B2** (re-pin carries `--board`): fixed. `stack-release.sh` drops `--board` when the target's `cli.mjs` lacks it,
|
||||
as `console_for_target` already does for `--tester-port`. The plan lists the drop. See decision 8.
|
||||
- **B3** (POST placement): fixed. The board POST branches sit above the refusal at `serve.mjs:558`, after the
|
||||
header and origin checks, next to bus message and chat. Loopback cannot reply or mark seen. See decision 5 and mutant M10.
|
||||
- **N1**: the board context uses the writer's default fleet source. See decision 9.
|
||||
- **N2**: the M2 check covers all of `packages/control-board/src`, not one file.
|
||||
- **N3**: the lines that change are listed by file in "Test line changes" below.
|
||||
- **N4**: the README records that loopback board reads stay open to any local process until Mos rules.
|
||||
- **N5**: row 198 lands after row 192. The board route tests rerun on the rebased tree.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **The Console calls the board's read code in-process.** A new module `packages/control-board/src/board.mjs`
|
||||
holds the route logic that `createServer` runs today: rescan, mark seen, reply and conversation read. It
|
||||
imports no `node:http` and opens no socket. Its methods are async (decision 10).
|
||||
2. **No listener anywhere in the board package.** `createServer` and `startServer` are deleted from
|
||||
`serve.mjs`. `serve.mjs` keeps its pure exports (`replyToRow`, `isLoopbackHost`, `foreignRequest`,
|
||||
`conversationResponse`, `REFUSAL_STATUS`, the reply constants). That keeps `packages/conversation/tests/flows.test.mjs`
|
||||
unchanged. The file name `serve.mjs` now misleads. Renaming it is a separate follow-up and is not in this row.
|
||||
3. **The writer's CLI refuses `serve`.** `packages/control-board/src/cli.mjs` keeps `scan`. `serve` exits 2 with a
|
||||
message that points to the Console. The default port 7331 goes away.
|
||||
4. **The Console gets the board by context, not by URL.** `startServer` in `packages/webui/src/serve.mjs` takes
|
||||
`board` as a context object (or `null` when no board is configured), not a URL. `DEFAULT_BOARD`, `boardURL`
|
||||
and the proxy `fetch` are removed.
|
||||
5. **`/api/reply` and `/api/seen` are gated by `--send`, and sit above the `:558` refusal.** Today the loopback Console
|
||||
forwards both POSTs with no `--send` check. That is a real gap, and this row closes it. Placement matters.
|
||||
In preview mode, `serve.mjs:558` refuses every non-GET before the board block, so a board branch below it never
|
||||
runs. In loopback mode, `--send` cannot be set at all (`:479`). So both POST branches go after the header and
|
||||
origin checks (`:493-516`), beside `/api/bus/message` (`:550`) and chat (`:554`), and require
|
||||
`send !== null && named`. The result: `/api/reply` is the first board write the live Console has ever allowed.
|
||||
Mos ruled it in (mos-1019), and mos-1023 item 5 confirms `/api/seen` too. In loopback mode, reply and mark-seen
|
||||
refuse with 403 `read-only`. Board reads still work there. `/api/seen` writes marks under the data root, and without
|
||||
the gate it would be the only preview-mode POST that passes without `--send`.
|
||||
6. **The retirement removes `--board` from the Console scripts.** No installed unit or live pointer carries `--board`
|
||||
(Sage checked, rev 3). So no live unit needs a flag change to start. The flag goes from `packages/webui/src/cli.mjs`
|
||||
(`:14`, `:18`, `:32`) and `scripts/webui-service.sh` (lines 44, 71, 110, 161-163). An old `--board` argument refuses
|
||||
with the usage error (exit 2). A re-pin with the flag removed is an ordinary re-pin. Cutover needs no unit edit.
|
||||
7. **The pinned copy carries the board code (B1).** The live Console runs from `copies/<sha>`. `COPY_PATHS` in
|
||||
`scripts/stack-copy.sh` (lines 23-33) lacks `packages/control-board/src` and `tools/tmux`. Without them a
|
||||
Console from a copy fails at import, and `reply` cannot find `agent-send.sh`. Both paths join `COPY_PATHS`.
|
||||
`tools/tmux` goes in whole, the same way `packages/plugins` does, because `agent-send.sh` may load its own
|
||||
siblings. The `control-board/src` closure is `conversation/src`, `discord/src` and `seat/src`, and those are already in
|
||||
the list.
|
||||
8. **Re-pin and rollback drop `--board` when the target cannot parse it (B2 guard).** The pointer's console flags
|
||||
(`P_CONSOLE`, `stack-release.sh:128`) pass through `:676` into `RUN_CONSOLE`, and `:459` passes them to
|
||||
`webui-service.sh install`. The live pointer has no `--board` (rev 3), so this is a guard for a pointer that
|
||||
carries one, such as an older pointer or a rollback target. Rule: keep `--board` only when the target's
|
||||
`packages/webui/src/cli.mjs` parses it. Otherwise drop it and report the drop in the plan. That is the same shape
|
||||
as `console_for_target` and `--tester-port` (`:667`, `:687-690`). Rollback to a copy that parses `--board` keeps
|
||||
the recorded flag. A copy that does not parse it (a new copy) gets the flag dropped and starts with no board URL,
|
||||
so its board is in-process or null, never 7331. The 7331 fallback applies only to an older copy that parses
|
||||
`--board` and has no recorded flag. That copy starts with its own default, 7331, which is dark, the same as today
|
||||
(Sage ruling 3).
|
||||
**Parse check (N2, rev 5).** "Parses it" means the target's `cli.mjs` holds the parser branch, `flag === '--board'`,
|
||||
not the flag name. The name also appears in the usage text and in refusal messages, so a plain `grep` would count
|
||||
a copy that refuses the flag as one that accepts it. One helper, `parses_flag FILE FLAG`, greps for
|
||||
`flag === 'FLAG'`. It is used for `--board` only, in `console_for_target` and the rollback path. The existing
|
||||
`--tester-port` grep at `stack-release.sh:670` stays as it is (Webby REVIEW-r4). Changing it would break the
|
||||
`stack-release.test.mjs:265` fixtures, which write a comment, not a parser line. No copy names `--tester-port` in a
|
||||
refusal today, so the gap exists only for `--board`.
|
||||
9. **The board context uses the writer's default fleet source (N1).** `--fleet` defaults to `~/.mosaic/fleet/agents`,
|
||||
as the writer did (`cli.mjs:14`). The Console reads it and never writes it. Whether the fleet is retired
|
||||
and the default becomes `none` is a later decision. This row keeps the board's contents unchanged.
|
||||
10. **The board's child calls are async (B1).** The Console runs the board in its own process. A blocking child call
|
||||
would stall every route, the event stream, `/healthz` and the tester listener, which shares the process
|
||||
(webui `cli.mjs:91`). Rule: no `spawnSync` on any path that `createBoard` reaches.
|
||||
- `scan.mjs` gains `tmuxInspectAsync` (same args, same 5 s timeout) and `scanAsync`. The sync `tmuxInspect` and
|
||||
`scan` stay for the CLI. The parse of the tmux output is one pure function that both paths call, so they cannot
|
||||
drift.
|
||||
- `serve.mjs` gains `replyToRowAsync` (same 15 s timeout, same env, same result shapes). The sync `replyToRow`
|
||||
stays for its existing tests. The reply trailer and status mapping are shared pure code.
|
||||
- The default async `exec` is `spawn` with a timeout that kills the child. A timeout or a spawn failure maps to
|
||||
`{ error }`, the same shape the sync path returns. Tests inject `exec`.
|
||||
- `index()` coalesces concurrent calls, but only onto a scan that started after the last write (rev 5, Webby N1).
|
||||
The board keeps a write counter. `markSeen()` and `reply()` bump it, then start a fresh scan and never join one.
|
||||
`index()` joins an in-flight scan only when that scan started at the current counter value. So an answer never
|
||||
omits a mark that was set before it was asked for. This coalescing is my choice to limit tmux calls under
|
||||
parallel reads; the design has no other constraint on it.
|
||||
- `conversation(url)` spawns nothing. It reads files on the event loop, as the writer did. Not changed in this row.
|
||||
|
||||
## Open points for Sage and Mos
|
||||
|
||||
- **Auth on loopback (Sage ruling 5, taken to Mos as sage-1370).** The Console has two modes. In preview mode, a named
|
||||
request needs the proxy header and proof, and the board routes inherit that. In loopback mode (no `--public-host`),
|
||||
there is no sign-in. Loopback stays read-only with no new gate in this row. Board reads stay open to any local
|
||||
process, as they are today. Reply and mark-seen cannot run there, because `--send` needs preview mode. The
|
||||
cross-uid loopback gate is row 196's. If Mos rules otherwise, Sage says so and this design changes.
|
||||
- **Row 192 overlap (Launchpad, SSO on `webui/src/serve.mjs`).** My edits touch these regions of
|
||||
`packages/webui/src/serve.mjs`:
|
||||
- lines 12-27 (`DEFAULT_BOARD`, `boardURL`)
|
||||
- line 466 (`startServer` signature, `board` option)
|
||||
- lines 473 (`upstream`)
|
||||
- lines 556-580 (board proxy block)
|
||||
- lines 598-610 (`/api/config` `board` field)
|
||||
|
||||
I do not edit the preview and proof gate (lines ~486-505) or the auth-header checks. The board branches go
|
||||
after those checks, in the same order as the bus and chat branches. Sage should sequence 192 and 198 so the
|
||||
second one rebases on the first. I will not touch the SSO code.
|
||||
- **Cutover.** The new Console code reaches the live Console through a copy and re-pin. The restart is a human
|
||||
step (`console-holds-human-authority`). No unit flag changes (decision 6). The live unit stays up until then.
|
||||
- **Sequencing (ruling 4, sage-1386).** Sage sequences 198 after 192 and after row 197 (Herb). I rebase once, onto the
|
||||
tree with 192 and 197 landed, and rerun the board route tests there. 197 replaces closed-endpoint ports with dynamic
|
||||
allocations. Its overlap with this row is `packages/webui/tests/serve.test.mjs:100` and
|
||||
`packages/webui/tests/preview.test.mjs:208`. My rewrites of those lines (the dead-board 503 change at serve.test.mjs:79
|
||||
and :105, and preview.test.mjs:219 in the N3 list) are written against 197's dynamic ports, not the old fixed ones.
|
||||
I do not edit either overlap line before 197 lands. The manifest goes to review only after the rebase.
|
||||
|
||||
## File-by-file
|
||||
|
||||
### Board package
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/control-board/src/board.mjs` | **New.** `createBoard({ specs, boardDir, seatsDir, discordDataRoot, isAlive, now, agentSend, exec, conversationReader })`. Returns `{ index(), markSeen(body), reply(body), conversation(url) }`. `index`, `markSeen` and `reply` return promises and use the async paths (decision 10). Each method is the body of a route that `createServer` runs now. No `node:http`, no listener. |
|
||||
| `packages/control-board/src/scan.mjs` | Adds `tmuxInspectAsync` and `scanAsync`, and one shared pure parse. Sync `tmuxInspect` and `scan` stay for the CLI. |
|
||||
| `packages/control-board/src/serve.mjs` | Delete `createServer` (lines 229-310), `startServer` (312-322), `page` loading (`loadPage`, 224-227), and the `node:http` import. Keep the pure exports and the sync `replyToRow`. Add `replyToRowAsync` (decision 10). Remove the `7331` default. |
|
||||
| `packages/control-board/src/cli.mjs` | `serve` exits 2 with `refused: the control board no longer serves HTTP. Use the Console: node packages/webui/src/cli.mjs serve`. The `--port` and `--host` flags are removed. `scan` is unchanged. Usage text is updated. |
|
||||
| `packages/control-board/src/page.html` | Delete. The Console serves its own `index.html`. |
|
||||
| `packages/control-board/README.md` | Remove the `serve` section (lines ~107, 127), the port default and the start instructions. Keep the reply-convention text (lines 144-151). It names no start command. |
|
||||
| `packages/control-board/package.json` | The `mosaic-board` bin keeps `scan` only. Description text updated. |
|
||||
|
||||
### Console
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/webui/src/serve.mjs` | Remove `DEFAULT_BOARD`, `boardURL` and the proxy `fetch`. `startServer({ board })` takes a context object or `null`. Board routes call `board.index()`, `board.markSeen()`, `board.reply()` or `board.conversation()` directly. If `board` is `null`, the board routes answer 503 with `{ error: 'no board configured' }`. `/api/reply` and `/api/seen` need `send !== null && named`, the same gate as the other writes. Without it they answer 403 `read-only`. `/api/config` returns `board: null` and no URL. The app reads `config.board` only for the footer text, so a `null` value needs a small change in `app.js:468`. |
|
||||
| `packages/webui/src/cli.mjs` | Remove `--board` and its usage text. Build the board context from the config, `--checkout` (or cwd, as the writer did), and the seats and board dirs. Failure to read the system config gives `board: null`, not a refusal. The Console still starts, as it does for the bus today. |
|
||||
| `packages/webui/src/public/app.js` | Line 468: keep the footer, show `Board: in-process` (or hide it). Lines 365, 468 and the 502 message text (`Board unreachable ... Check the board server`) are reworded. The text is not a contract. |
|
||||
| `packages/webui/src/public/index.html` | No change. |
|
||||
| `packages/webui/README.md` | Remove the start line (10), the `--board` line (15), the test command that lists `control-board/tests/` (766, keep only the in-process tests), and the rollback line (881). |
|
||||
| `packages/webui/systemd/mosaic-webui.service.in` | No change in the template. The `@FLAGS@` set loses `--board`. |
|
||||
| `scripts/webui-service.sh` | Remove `--board` from the usage (44, 71), the parser (110), the validation (161-162) and the flag (163). An old `--board` argument refuses with the usage error. |
|
||||
| `scripts/stack-copy.sh` | **B1.** `COPY_PATHS` (23-33) adds `packages/control-board/src` and `tools/tmux`. |
|
||||
| `scripts/stack-release.sh` | **B2.** `console_for_target` (around 664-692) drops `--board` when the target's `cli.mjs` does not parse it. The plan line names the drop. Rollback keeps the recorded flag when the target parses it. |
|
||||
| `packages/webui/README.md` | Also records (N4) that loopback board reads stay open to any local process until Mos rules. |
|
||||
| `packages/webui/src/public/bus.js` | No change. The `#/` board link is the Console's own page, not 7331. |
|
||||
|
||||
### Tests (webui)
|
||||
|
||||
Fixture plan. "Dead board" means a context whose `index()` throws, which replaces `board: 'http://127.0.0.1:9'`.
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/webui/tests/fixture.mjs` | Call `createBoard` directly. Pass the context to `startServer`. No port 0 board server. |
|
||||
| `packages/webui/tests/history-fixture.mjs` | Same as fixture.mjs. |
|
||||
| `packages/webui/tests/discord.test.mjs` | Same. Board server removed. |
|
||||
| `packages/webui/tests/relaunch.test.mjs` | Same. |
|
||||
| `packages/webui/tests/return-flow.test.mjs` | Same. |
|
||||
| `packages/webui/tests/tester-fixture.mjs` | `board: 'http://127.0.0.1:9'` becomes the dead board. |
|
||||
| 16 files with `board: 'http://127.0.0.1:9'` (bus, chat, instances-browser, limits-browser, messages, pinned-copy, preview, proof-cli, proof, routes-browser, send, settings-form, tester-cli, tester-listener, usage-browser, views) | Mechanical: dead board. Assertions that expect a 502 `Board unreachable` string are updated to the new 503 `no board configured` or the dead-board error text. Sage needs to confirm this count before the edit. |
|
||||
| `packages/webui/tests/serve.test.mjs` | Lines 17-19 (`boardURL` checks) are removed. Add the new board route tests (below). |
|
||||
| `packages/webui/tests/reads.test.mjs` | Line 188: the `board` setting becomes `null`. |
|
||||
| `packages/webui/tests/service.test.mjs` | Lines 225-394: unit `ExecStart` text loses `--board`. The `--board` refusal cases (250, 260, 394) become refusal of the old flag. |
|
||||
| Browser tests (`browser-edge`, `bus-browser`, `chat-browser`, `docs-browser`, `editor-browser`) | Fixture context only. No change to the browser assertions. |
|
||||
|
||||
### Tests (control board)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/control-board/tests/serve.test.mjs` | 32 tests use HTTP. Move the route assertions to `board.test.mjs`, which calls `createBoard` directly. Keep the `replyToRow` tests here. |
|
||||
| `packages/control-board/tests/discord.test.mjs` | Two HTTP uses (lines 123 and the `startServer` call). Call `createBoard`. |
|
||||
| `packages/control-board/tests/scan.test.mjs`, `attention*.test.mjs`, `relaunch.test.mjs` | No change. They test `scan`, not HTTP. |
|
||||
| `packages/control-board/tests/retired.test.mjs` | **New.** Three checks, all in-process, no socket. (1) `cli.mjs serve` exits 2 and prints the Console pointer. (2) The module graph has no `node:http` import and no `listen(` call in `packages/control-board/src`. (3) A grep over the start paths (`scripts/`, `*.service*`, `packages/*/README.md`, `package.json` scripts) finds no `control-board.*serve` or `7331` starter. |
|
||||
|
||||
### New tests for the acceptance items
|
||||
|
||||
| Test | Where | Assertion |
|
||||
|---|---|---|
|
||||
| Board view answers with nothing on 7331 | `packages/webui/tests/board-inprocess.test.mjs` (new) | Console with a context returns 200 on `/api/board` with a stubbed `globalThis.fetch` that throws on any call. Zero fetch calls. |
|
||||
| Every board route refuses without the Console's gate | same file | In preview mode, `/api/board`, `/api/seen`, `/api/reply`, `/api/conversations` each return 401 without the header and 403 with a foreign Host. |
|
||||
| `/api/reply` refuses without `--send` | same file | Preview mode, no `send`. POST `/api/reply` returns 403 `read-only`. `replyToRow` is never called (an injected `exec` counts calls). |
|
||||
| `/api/seen` refuses without `--send` | same file | Same shape. No mark file written. |
|
||||
| `/api/reply` runs with `--send` | same file | Positive control: with `send` and the sign-in header, the injected `exec` is called once with the expected argv. |
|
||||
| No Console proxy left | `packages/webui/tests/board-inprocess.test.mjs` | `DEFAULT_BOARD` is not exported. `boardURL` is not exported. Grep check: no `7331` in `packages/webui/src`. |
|
||||
| markSeen answer includes its own mark (N1, rev 5) | `packages/control-board/tests/board.test.mjs` (new) | Hold a scan in flight with an injected exec that resolves late. Call `index()`, then `markSeen()` before the scan resolves. The `markSeen()` answer shows the mark. A mutant that joins the pre-write scan must fail it. The same check covers `reply()`. |
|
||||
| Board does not block the Console (B1) | same file | Injected async `exec` for the tmux check resolves after 2 s. While `/api/board` is pending, `/healthz` answers within 200 ms. Mutant M14 uses an `exec` that blocks the thread for 2 s with `Atomics.wait`, and this test must kill it. |
|
||||
|
||||
## Mutants
|
||||
|
||||
Each mutant runs on a scratch copy of the tree (not the live checkout), with the new tests. The test that
|
||||
must kill it is listed. A mutant that survives means the test is weak.
|
||||
|
||||
| # | Mutant | Must be killed by |
|
||||
|---|---|---|
|
||||
| M1 | `cli.mjs serve` calls `startServer` again on 7331 | `retired.test.mjs` (1) |
|
||||
| M2 | `serve.mjs` restores `createServer` or `startServer` and exports them | `retired.test.mjs` (2) |
|
||||
| M3 | Console `DEFAULT_BOARD` is restored and the proxy `fetch` returns | `board-inprocess.test.mjs` zero-fetch case |
|
||||
| M4 | `/api/reply` skips the `send` check | `/api/reply refuses without --send` |
|
||||
| M5 | `/api/seen` skips the `send` check | `/api/seen refuses without --send` |
|
||||
| M6 | The board block runs before the preview and header gate | "every board route refuses" (401 case) |
|
||||
| M7 | A board route accepts a foreign Host | "every board route refuses" (403 case) |
|
||||
| M8 | `scripts/webui-service.sh` re-accepts `--board` | `service.test.mjs` refusal case |
|
||||
| M9 | A README or script again tells people to run `control-board serve` | `retired.test.mjs` (3) |
|
||||
| M10 | The `/api/reply` and `/api/seen` branches move below the `:558` refusal | The positive control "`/api/reply` runs with `--send`" (403 from `:558`) |
|
||||
| M11 | `COPY_PATHS` drops `packages/control-board/src` or `tools/tmux` | Fresh-copy test in `pinned-copy.test.mjs`: `/api/board` must answer 200 |
|
||||
| M12 | `console_for_target` keeps `--board` for a target that does not parse it | `stack-release.test.mjs`: re-pin plan lists the drop; install argv has no `--board`. The target `cli.mjs` names `--board` only in a refusal message and has no parser branch (N2). A grep-based check keeps the flag and this test kills it. |
|
||||
| M15 | `index()` joins a scan that started before the last write (N1) | the `markSeen` answer case in `board.test.mjs` |
|
||||
| M13 | Rollback drops a recorded `--board` from a target that parses it | `stack-release.test.mjs`: rollback keeps the flag |
|
||||
| M14 | The board's tmux check or reply runs through a blocking call (`spawnSync`, or an `exec` that blocks the thread) | `board-inprocess.test.mjs` B1 case: `/healthz` must answer within 200 ms while `/api/board` is pending |
|
||||
|
||||
Mutants M11 to M13 run in the cli suite, which needs Sage's slot. M10 and M14 run in one webui test file each.
|
||||
|
||||
## Test additions (B1, B2)
|
||||
|
||||
| Test | File | Assertion |
|
||||
|---|---|---|
|
||||
| Fresh-copy Console answers the board | `packages/webui/tests/pinned-copy.test.mjs` (extend) | `stack-copy.sh` builds a copy at a fixed sha in a temp dir. Starts the Console from it on port 0. `GET /api/board` returns 200 JSON. `agent-send.sh` resolves inside the copy. |
|
||||
| Re-pin drops `--board` | `packages/cli/tests/stack-release.test.mjs` (extend) | Pointer records `--board`. Target `cli.mjs` lacks the parser branch, and names `--board` only in a refusal (the N2 fixture). The plan names the drop. The install argv has no `--board`. |
|
||||
| Rollback keeps `--board` | same | Rollback to a copy with `flag === '--board'` in its `cli.mjs` keeps the recorded flag. |
|
||||
|
||||
## Test line changes (N3)
|
||||
|
||||
Board-related lines that change, by file:
|
||||
|
||||
- `packages/webui/tests/serve.test.mjs:79`: `/api/board` on a dead board, 502 becomes 503.
|
||||
- `packages/webui/tests/serve.test.mjs:105`: 502 with the board URL in the body. Becomes 503 with no URL.
|
||||
- `packages/webui/tests/preview.test.mjs:219`: dead board, 502 becomes 503. `board.body.board` is no longer a URL.
|
||||
- `packages/webui/tests/send-browser.test.mjs:89`: a `502 board unreachable` string. Verify at edit time. Likely a bus status, which stays.
|
||||
- `packages/webui/tests/serve.test.mjs:17-19`: `boardURL` checks, removed.
|
||||
|
||||
Unchanged, not board: `bus.test.mjs:226-236`, `chat.test.mjs:171-179`, `instances.test.mjs:163-168`,
|
||||
`limits-route.test.mjs:35`, `project-file.test.mjs:128`, `routes.test.mjs:170`, `send.test.mjs:230`,
|
||||
`tester-cli.test.mjs:57`, `tester-listener.test.mjs:113`, `usage.test.mjs:24`, `board-held.test.mjs:204` (page text).
|
||||
|
||||
Dead-board fixture files (16 webui plus `tester-fixture.mjs`) match Webby's count.
|
||||
|
||||
## Gate
|
||||
|
||||
Mutant runs use single test files, which run freely. Any suite (control-board, webui, cli, every
|
||||
`scripts/test-*.sh`) needs Sage's slot.
|
||||
|
||||
Webby approves INVENTORY.md (already sent), then this DESIGN.md, then the manifest. Sage's gate runs the
|
||||
control-board and webui suites, the cli suite and every `scripts/test-*.sh`. All must be green, and mutants
|
||||
M1-M15 killed.
|
||||
|
||||
## Stop points
|
||||
|
||||
- If Mos rules that loopback board reads need a gate, this design changes. Sage tells me, and I stop and report.
|
||||
- If the Console's auth model changes under row 192, the board block in section "Console" changes with it.
|
||||
- The Console restart and unit re-render are Jason's or Mos's steps. I do not run them.
|
||||
|
||||
## Boundaries kept
|
||||
|
||||
No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files, `~/.mosaic`, or any live port.
|
||||
No request to 7331 or 7332 at any point.
|
||||
@@ -1,125 +0,0 @@
|
||||
# Row 198 inventory: control board writer and port 7331
|
||||
|
||||
Gitea #1696. Brief: `docs/plans/2026-10-20_retire-control-board.md`. Owner Bushroot, reviewer Webby, gate owner Sage.
|
||||
Scope: tracked files at `bushroot/row198` (base `refactor`). Excludes `v1/` and other seats' `agents/*/work/` packets.
|
||||
Method: `git grep -I` for `control-board`, `control_board`, `controlboard` and `7331`, then a read of each code hit.
|
||||
|
||||
## Classes
|
||||
|
||||
- **starts**: runs `packages/control-board` `serve`, or a unit, script or default that does.
|
||||
- **imports**: loads code from `packages/control-board` at run time or test time.
|
||||
- **mentions**: text only (docs, comments, fixture strings, history).
|
||||
|
||||
## Result
|
||||
|
||||
**Live dependency found. Stop before any removal.** The Console (`packages/webui`) proxies its board reads and
|
||||
writes to the writer on 7331. The Console's board view, `/api/board`, `/api/seen`, `/api/reply` and the
|
||||
conversation routes all forward to `DEFAULT_BOARD = http://127.0.0.1:7331`. The Console has no board without
|
||||
a writer on that port. Mos decides before anything is removed.
|
||||
|
||||
Nothing in the repository runs `control-board serve` from a unit. The only starter is the manual command in
|
||||
`packages/control-board/README.md` and `packages/webui/README.md`. `ss -ltnp` showed nothing on 7331 at
|
||||
2026-10-20T19:05:09Z (brief).
|
||||
|
||||
## Hits
|
||||
|
||||
### Starts the writer, or defaults to its port
|
||||
|
||||
| File | Line | Class | What it does |
|
||||
|---|---|---|---|
|
||||
| `packages/control-board/src/cli.mjs` | 4, 14 | starts | `serve` subcommand; default `port: 7331` |
|
||||
| `packages/control-board/src/serve.mjs` | 312 | starts | `startServer` default `port = 7331` (the writer itself) |
|
||||
| `packages/control-board/README.md` | 107, 127 | starts | Tells people to run `serve` and open 7331; documents the default |
|
||||
| `packages/webui/README.md` | 10 | starts | Tells people to run `node packages/control-board/src/cli.mjs serve` |
|
||||
| `packages/webui/README.md` | 15, 881 | depends | Console reads the board at 7331; rollback uses "the unchanged board at 7331" |
|
||||
| `packages/webui/src/cli.mjs` | 14, 18 | depends | `--board` default is `DEFAULT_BOARD` (7331) |
|
||||
| `packages/webui/src/serve.mjs` | 12, 473, 561, 578, 610 | **depends (live)** | `DEFAULT_BOARD`; proxies `GET /api/board`, `GET /api/conversation(s)`, `POST /api/seen`, `POST /api/reply` upstream; returns 502 if the writer is absent |
|
||||
| `packages/webui/src/public/app.js` | 468, 365 | depends | Page reads `/api/config` board, then `/api/board` through the Console proxy |
|
||||
| `packages/webui/src/public/bus.js` | 1037, 1088, 1100 | depends | Routes `#/` to the board page; bus pages link to it |
|
||||
| `scripts/webui-service.sh` | 89-163 | depends | Renders the Console unit with `--board URL`; default comes from `serve` |
|
||||
| `packages/webui/systemd/mosaic-webui.service.in` | 29 | depends | Unit runs the Console (`webui/src/cli.mjs serve`), not the writer; flags carry `--board` |
|
||||
| `scripts/stack-copy.sh` | 88 | mentions | Checks the Console `ExecStart` string; no writer reference |
|
||||
| `scripts/stack-release.sh` | 1372 | mentions | Checks the Console command line; no writer reference |
|
||||
|
||||
### Imports code from `packages/control-board`
|
||||
|
||||
| File | Line | Class | What it imports |
|
||||
|---|---|---|---|
|
||||
| `packages/webui/tests/fixture.mjs` | 6 | imports | `startServer` from `control-board/src/serve.mjs` (test fixture for the Console) |
|
||||
| `packages/webui/tests/history-fixture.mjs` | 8 | imports | same |
|
||||
| `packages/webui/tests/discord.test.mjs` | 6 | imports | same |
|
||||
| `packages/webui/tests/relaunch.test.mjs` | 6 | imports | same |
|
||||
| `packages/webui/tests/return-flow.test.mjs` | 13 | imports | same |
|
||||
| `packages/conversation/tests/flows.test.mjs` | 15 | imports | `replyToRow` from `control-board/src/serve.mjs` |
|
||||
|
||||
The webui tests run the writer's server in-process. They do not start the writer on 7331 (the tests bind
|
||||
their own port), but they need `serve.mjs`. Any removal or refusal in `serve.mjs` breaks these tests unless
|
||||
the writer's read-only code moves first.
|
||||
|
||||
`packages/webui/src/` (non-test code) does not import `control-board`. Only the tests do.
|
||||
|
||||
### Tests that name the writer or its files
|
||||
|
||||
| File | Line | Class | Note |
|
||||
|---|---|---|---|
|
||||
| `packages/control-board/tests/serve.test.mjs` | 37, 744-767 | imports | Tests the writer: reply runner, `REPLY_TRAILER`, `-S <host>:control-board` |
|
||||
| `packages/control-board/tests/scan.test.mjs` | 47, 306, 353 | mentions | Tmp dir names and comments |
|
||||
| `packages/webui/tests/serve.test.mjs` | 18-19, 35 | mentions | `boardURL` checks on port 7331; reply trailer string |
|
||||
| `packages/webui/tests/reads.test.mjs` | 188 | mentions | Expected `board` setting value 7331 |
|
||||
| `packages/webui/tests/service.test.mjs` | 225-394 | mentions | Unit `ExecStart` text includes `--board http://127.0.0.1:7331` |
|
||||
| `packages/plugins/tests/harness.test.mjs` | 173 | mentions | Path string `packages/control-board/src/scan.mjs` |
|
||||
| `packages/ledger/tests/ledger.test.mjs` | 80, 233-327 | mentions | Sender name `control-board` as a test fixture |
|
||||
| `packages/queue/tests/fixtures/genesis-render.md` | 6-11 | mentions | Fixture text |
|
||||
| `packages/conversation/tests/reader.test.mjs` | 2 | mentions | Comment |
|
||||
| `packages/conversation/README.md` | 207 | mentions | Says the control-board suite serves the routes |
|
||||
|
||||
### Code that reads the sender name, not the writer
|
||||
|
||||
| File | Line | Class | Note |
|
||||
|---|---|---|---|
|
||||
| `packages/ledger/src/ledger.mjs` | 120 | mentions | Classifies `control-board` as sender kind `board`. No import. |
|
||||
|
||||
### Package and script metadata
|
||||
|
||||
| File | Line | Class | Note |
|
||||
|---|---|---|---|
|
||||
| `packages/control-board/package.json` | 2, 5, 9 | starts | Declares `bin: mosaic-board -> src/cli.mjs` (a `mosaic-board serve` entry point) |
|
||||
| `package.json` (root) | - | none | No script names the board or 7331 |
|
||||
| `scripts/mosaic` | 3 | mentions | Comment only |
|
||||
| `packages/seat/package.json` | 5 | mentions | Description: "the control board reads" registration |
|
||||
| `packages/seat/README.md` | 139 | mentions | Says the board reads `registration.json` |
|
||||
| `packages/plugins/README.md` | 181 | mentions | Names `scan.mjs` in an exception note |
|
||||
|
||||
### Docs (mention only)
|
||||
|
||||
- `AGENTS.md` lines 144-150: reply convention for control-board attention. Names the package README. Does
|
||||
not tell anyone to start 7331. The brief says to change it only if it does.
|
||||
- `docs/SESSIONS.md` (32), `BUILD-LOG.md` (76), `docs/plans/CURRENT.md` (8), `docs/plans/QUEUE.md` (12),
|
||||
`docs/plans/queue.json` (45), `docs/plans/2026-09-12_control-board-mvp.md` (21),
|
||||
`docs/plans/2026-10-*` and other plan pages, `docs/plans/reviews/*`, `docs/design/*`.
|
||||
- `docs/plans/CURRENT.md:171` says "Start it with `node packages/control-board/src/cli.mjs serve`". That is
|
||||
an instruction to start the writer. Update it when the writer is retired.
|
||||
- `docs/plans/2026-10-18_containerized-install-design.md:927` and `-rows.md:213` cite `serve.mjs` line numbers.
|
||||
- `docs/plans/2026-10-19_seen-click-intent.md:29` cites `serve.mjs:257` (`/api/seen`).
|
||||
- `BUILD-LOG.md` is append-only. Leave it alone.
|
||||
- `agents/bushroot/README.md`, `agents/README.md`, `docs/design/README.md`, `docs/design/IMPLEMENTING.md`:
|
||||
mention only.
|
||||
- `packages/webui/third_party/editor/PROVENANCE.json`: mention only (the vendored editor's clipboard, not the board).
|
||||
- `packages/webui/src/public/*` clipboard, board-view and CSS names: unrelated to the writer.
|
||||
|
||||
### Excluded
|
||||
|
||||
- `v1/*`: archived.
|
||||
- `agents/*/work/*`: other seats' packets (darkwing, filbert, rocko and others). Not read for this inventory.
|
||||
|
||||
## Open items for Mos
|
||||
|
||||
1. The Console's board view and proxy routes need a writer on 7331 or an equivalent read path. Retiring the
|
||||
writer means choosing a replacement first, or accepting that the board view goes dark.
|
||||
2. Four plan pages and `CURRENT.md:171` still tell people to start the writer.
|
||||
3. The webui tests and `conversation/tests/flows.test.mjs` import `serve.mjs`. Any change to it needs those
|
||||
tests moved first.
|
||||
|
||||
## Sha256
|
||||
|
||||
Recorded in the send message after commit. This file is the inventory; the commit carries it.
|
||||
@@ -3,5 +3,5 @@
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0977, thread ffceafec). Reviews federation designs and MVP T1 row 193.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `duckworth/*` branches
|
||||
credentials of its own; until row 195 settles per-agent identity, it pushes only its own `duckworth/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Gander
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for the human
|
||||
boundary row (P19 333, #1694, mos-0989).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `gander/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Herb
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane
|
||||
design row (#1695, mos-0977 item 6).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `herb/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -3,5 +3,5 @@
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 194 (agent lifecycle, #1692) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `honker/*` branches
|
||||
credentials of its own; until row 195 settles per-agent identity, it pushes only its own `honker/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 192 (Q175 SSO phase 1, #1690) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `launchpad/*` branches
|
||||
credentials of its own; until row 195 settles per-agent identity, it pushes only its own `launchpad/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 193 (human users and RBAC, #1691) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `morgana/*` branches
|
||||
credentials of its own; until row 195 settles per-agent identity, it pushes only its own `morgana/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
# 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 f7472d1e47b1). 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
|
||||
|
||||
```sql
|
||||
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.
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
n=0
|
||||
for p in /proc/[0-9]*; do
|
||||
c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue
|
||||
case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac
|
||||
if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi
|
||||
done
|
||||
[ $n -eq 0 ] && echo CLEAR
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment.
|
||||
# Sequential suites, each teed to its own log; stops at the first failure.
|
||||
G=/mnt/storage/scratch/SET-ME-wt
|
||||
OUT=/mnt/storage/scratch/SET-ME-out
|
||||
cd "$G" || exit 9
|
||||
mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15)
|
||||
# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt"
|
||||
# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit.
|
||||
export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274).
|
||||
for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue
|
||||
run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done
|
||||
for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done
|
||||
echo GATE_PASSED
|
||||
@@ -3,5 +3,5 @@
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0981, thread 03086006). Reviews Case 2 designs and MVP T1 rows 192 and 194.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `webby/*` branches
|
||||
credentials of its own; until row 195 settles per-agent identity, it pushes only its own `webby/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
|
||||
@@ -1361,5 +1361,3 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-20T18:34:53Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r2 (DESIGN.md blob 3eb718b5e131 at 1a268249e), static | changes: C1 config.json console key fails old loaders (mosaic-config.mjs:113 rejects unknown keys; key add inside phase 2 window, rollback removes it, loader test), C2 app.js signs out only on opaqueredirect so 401 never shows sign-in; notes on tests 15/7/9a, Q-B 200-HTML classification, scratch Console flags; Q-E keep iss/sub escaped and capped; verdict sent to Sage, no Gitea/queue record
|
||||
2026-10-20T18:40:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r3 (DESIGN.md blob a152fd58e18e at 817e8f51b), static | approve on condition D1 (slot file checks stay out of shared loader, shape-only there) as a line edit; D2 notes: slot root from configDir, scratch dataRoot, name discovery GETs for Mos; C1/C2 and notes verified; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:41:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design D1/D2 check (DESIGN.md blob eba861f5d2bb at d55d86bb0), static git diff 817e8f51b..d55d86bb0 | ack: D1 and D2 match; design approved; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
|
||||
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Brief: Melanie's hub instance on its own host (row 195, was 190b)
|
||||
|
||||
Sage, 2026-10-20. Rewritten for Mos's rulings mos-0998 and mos-1005 (Vikunja #418, #419). The first version
|
||||
put the hub under its own Linux user on dragon-lin. That form can't pass Duckworth's B7 there, because the
|
||||
factory uid's `sudo -n` succeeds on dragon-lin (Mos's recon, brain-redux
|
||||
`projects/mosaic-mvp/MELANIE-HOST-RECON.md`). Melanie's own server is ruled out too: it has 1.2 GiB RAM free
|
||||
with her two Hermes bots running, and co-tenancy would share her user and her passwordless sudo. The hub runs
|
||||
on a separate host. Which host is Jason's decision (#419): hardware outside the cluster, a cluster VM with
|
||||
factory PVE root removed, or a cluster VM with that residual accepted. This row is written so the host choice
|
||||
doesn't change it. Gitea #1693. On the 10-27 MVP path. Federation R8 (Gizmoduck's design r4, 04a6112be) needs
|
||||
this row or nothing.
|
||||
|
||||
Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5 and B7), gate owner Sage. Chained on row 190
|
||||
(Q177): the hub runs under row 190's config canon. Design first. The first deliverable is
|
||||
`agents/darkwing/work/queue-190b/DESIGN.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Melanie's hub instance (Case 2, C2-13) runs on its own host. No factory agent can read or write the hub's
|
||||
config, data root or secrets, reach its bus socket, or log in to the hub host. Her writing reaches the hub only
|
||||
through the existing git.mosaicstack.dev remotes, books repo only.
|
||||
|
||||
## Provision (blank until Jason answers #419)
|
||||
|
||||
Who provisions the host and how is left open. Sizing from Mos (mos-0998): 4 vCPU, 8 GB RAM, 64 GB disk,
|
||||
internal network only. Each agent process measures 300 to 390 MB RSS on dragon-lin. The design takes the host
|
||||
as given: a fresh Linux host with a named admin path, reachable by the deploying seat while the row runs.
|
||||
|
||||
## The design must state
|
||||
|
||||
1. The deploy. Exactly what Darkwing installs on the hub host and how: the stack copy, node, the units, the
|
||||
config dir and data root under row 190's canon (O1: `~/.config/mosaic-NAME`, and "`~` is the home of the OS
|
||||
user that runs the instance"). Which commands run as which user on the hub host, and which need its root.
|
||||
2. Re-pin, rollback and backup on the hub host, and who runs them after the deploy key is gone.
|
||||
3. How the hub gets model access without a copy of jwoltje's `claude-session.env` or any factory credential.
|
||||
If that needs the credentials row (T1 item 6), say so and name the interim. If it needs a new subscription
|
||||
or a new provider account, flag it for Jason and don't pick one (mos-1008).
|
||||
4. The books repo path. Which git.mosaicstack.dev remote, which credential the hub holds for it, and that it
|
||||
can't read or push any other repository.
|
||||
5. The admin path and the end state. The host ends with no key the factory uid can read. The design names the
|
||||
admin path that remains (for example the hypervisor console, or a key kept off the factory uid) and who
|
||||
holds it.
|
||||
6. The acceptance script, as code Duckworth can read. It takes Gizmoduck's list of infrastructure gate items
|
||||
as its input (mos-1011), not a fixed set of checks. Each item names a path, its closed form and its
|
||||
accepted form, and the script reports which form holds for each, or fails. The list covers at least:
|
||||
- federation R8's separate-host form: the hub's bus socket, data root and config dir don't exist on the
|
||||
factory host, the factory host mounts nothing of the hub's, and no factory agent holds a login or key
|
||||
for the hub host
|
||||
- B7: from the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub
|
||||
host all fail
|
||||
- the hypervisor and backup paths ops-01 measured (mos-1011: one PVE node the factory key has root on,
|
||||
PBS on that node with unencrypted backups, a flat /16 LAN), each closed or accepted by Jason
|
||||
7. The identity provider and TLS front end (B8, mos-1008, mos-1011). Either the factory holds no credential
|
||||
that can create users, change groups, issue tokens or impersonate in the issuer the hub trusts, or the hub
|
||||
uses an issuer the factory can't administer. A brand, tenant or app inside the same Authentik does not
|
||||
count as separate. The same holds for the TLS front end that serves the hub. The proof is a failed admin
|
||||
call from every token the factory's root can read. No credential value is printed in that proof.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Provisioning, hypervisor access and any infra or k3s change are Jason's (Q173 exclusions). Mos takes the
|
||||
hypervisor residual (factory root on the PVE nodes, Q172) to Jason. It doesn't block this row.
|
||||
- Melanie's server (`[email protected]`) is reference only, read only, under Jason's authorization in
|
||||
mos-0995. Nothing on it changes, and her Hermes bots keep running.
|
||||
- No tunnel, Traefik or other exposure outside the internal network. That waits for Jason, after Q175 SSO.
|
||||
- No credential value is printed, logged or committed. Keys made for the deploy are removed at the end, and
|
||||
the acceptance script proves it.
|
||||
- The admin-call proof in point 7 and any call to the issuer or the TLS front end run only in a window Mos
|
||||
approves, never as a test or an anonymous probe (Q175).
|
||||
- Tests in the repository use scratch roots and stand-ins. The live deploy and the acceptance run on the hub
|
||||
host are a window with a backup first (where there is state) and a STATE report after.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and
|
||||
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the acceptance script passes
|
||||
on the hub host from the factory uid, Duckworth reads its output, and Mos confirms.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Brief: the human boundary across uids and humans (P19 333)
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0989 (P19 333, MOSAICSTA-173, is now on the 10-27 path: federation R3 and
|
||||
row 193 need it). Gitea #1694. Owner Gander (Codex gpt-6.1-sol, a new builder seat under mos-0977 item 5),
|
||||
reviewer Duckworth, gate owner Sage. Latest-by: DESIGN.md approved by 2026-10-22, landed by 2026-10-24, and in
|
||||
any case before federation R3 lands. R3 and row 193 may chain on this row's candidate (Q177). Base: refactor at
|
||||
the brief commit. Design first. The first deliverable is `agents/gander/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## The problem, as filed and as it now matters
|
||||
|
||||
P19 333 as filed (row 87, mos-0657): the Console listens on a loopback TCP port, and any host process that
|
||||
reaches it, under any uid, acts as Jason. Jason accepted that as a documented limit for a one-user host
|
||||
(mos-0787, row 161). Two things change that:
|
||||
|
||||
- Row 190b (#1693, mos-0986) puts Melanie's hub under its own Linux user. Factory processes then sit on another
|
||||
uid, on the same host, next to the hub's ports and socket. The other-uid reach is now the Case 2 threat.
|
||||
- Federation r2 (gizmoduck-0004, 60b9b112a) binds a bus user only from a user-cli whose proof passes the human
|
||||
walk, whose direct parent is the bus's own Console, and which holds a live grant. That rule rests on the
|
||||
path-bound human proof (#225): today a same-uid process that runs the tree's `human-cli.mjs` acts as the
|
||||
human, without touching the Console. The Q175 design (section 8) names that limit and leaves it here.
|
||||
|
||||
## What already settles part of it
|
||||
|
||||
- Q175 phase 2 (#412) settles the Console-ingress part on the main port. Under `--sso`, 7332 answers any
|
||||
non-public Host with 403 except `GET /healthz`, and every other request needs a verified OIDC session. A
|
||||
process on another uid that reaches the port gets only `/healthz`. Phase 1 (row 192) builds it; it is live
|
||||
only after the phase 2 window.
|
||||
- Row 189 (`--no-send`) is interim only. Until phase 2 the Console takes no human writes, so the reach is
|
||||
read-only. It settles nothing.
|
||||
- Row 190b separates files and units by uid. Whether the hub's `broker.sock` refuses the factory uid depends on
|
||||
its directory and socket modes, which this row checks and tests rather than assumes.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Inventory every listener and entry point per instance: the Console port, the tester listener (7333 today),
|
||||
`broker.sock`, the human CLI wrapper, the Discord connector and anything row 190 adds. For each: who can
|
||||
reach it today from the same uid and from another uid, with and without `--sso`.
|
||||
2. The tester listener. It serves reads and the row 175 tester writes to anything that reaches it, on any uid.
|
||||
Close the other-uid reach (for example a unix socket with 0600 modes, or a peer-uid check on loopback), or
|
||||
bring Mos a concrete reason it must stay open. bus.tester stays Jason's switch.
|
||||
3. The bus human boundary. How the bus tells the human's own verified call from a same-uid process that runs
|
||||
`human-cli.mjs`. State what federation R3's direct-parent rule needs from the bus (peer credentials on
|
||||
`broker.sock`, the parent check, how it can't be forged by a child of an agent), and what stays possible for
|
||||
a same-uid process after this row. Say it plainly where a residual limit remains.
|
||||
4. Fit with row 193 (Morgana, human users and roles; Mos's B1 rule: a principal comes only from something the
|
||||
bus verifies) and with federation R3. One mechanism, not three.
|
||||
5. Migration: the live single-user Console and bus keep working until each instance re-pins.
|
||||
6. Which parts of P19 333 this row closes, which parts Q175 phase 2 closes, and what P19 333 says when it is
|
||||
closed.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Who can reach the Console from outside stays Jason's. This row changes what a host process can do once it
|
||||
reaches a local port or socket.
|
||||
- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or a slot file. No test creates a real
|
||||
second uid; use stand-ins as rows 190 and 190b do, and name any part only a Q173 window can prove.
|
||||
- No request to the live 127.0.0.1:7332 or 7333, mosaic.woltje.com or auth.diversecanvas.com.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. The bus, webui and cli suites and every `scripts/test-*.sh`
|
||||
are green on Sage's gate, and the design's mutants are killed.
|
||||
@@ -12,10 +12,8 @@ before any code, as Q175 did. Then the build follows the design, with tests and
|
||||
|
||||
Two or more instances run side by side on dragon-lin. Each one has its own config file, data root (store,
|
||||
broker.sock, host.lock, state, runs, sessions, workspaces), businesses, bus unit, Console unit and port,
|
||||
tester port, Discord unit, Claude session env file, stack-active pointer and activation log. No instance's
|
||||
commands, units or tools address another instance's state by default or by mistake. On one uid this is
|
||||
addressing only, not a security boundary: a same-uid process can still read another instance's files.
|
||||
Isolation against that is row 190b (#1693, mos-0986), one Linux user per instance. The current instance keeps working unchanged: its
|
||||
tester port, Discord unit, Claude session env file, stack-active pointer and activation log. Nothing one
|
||||
instance does can read or change another's state. The current instance keeps working unchanged: its
|
||||
config path, data root, unit names and ports stay as they are, and no re-pin is needed for it.
|
||||
|
||||
## Known single-instance points (GAP-MAP.md, measured at 9ae8c73ac)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Brief: retire the control board writer on 7331
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-1008 item 2. Gitea #1696. MVP 10-27 work. Owner Bushroot (Haiku 5.5, a
|
||||
low-level model under Q177), reviewer Webby, gate owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
## Why
|
||||
|
||||
`packages/control-board/src/serve.mjs` is a loopback HTTP service on 7331 with no authentication. It reads
|
||||
transcripts and writes through `/api/reply`. Any host process that reaches the port can do both. That fails
|
||||
Q175 ("zero trust with verification"). Gander found it in the row 196 inventory (gander-0004). Nothing listens
|
||||
on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least.
|
||||
|
||||
## Steps, in order
|
||||
|
||||
1. Inventory first. List every reference to the control board and to 7331: scripts, units, `scripts/mosaic`
|
||||
and other CLI dispatch, docs (AGENTS.md, READMEs, TOOLS.md), package.json scripts, tests, and the Console
|
||||
(`packages/webui`). For each, say whether it starts the writer, reads its code, or only mentions it.
|
||||
Commit the list as `agents/bushroot/work/queue-ROW/INVENTORY.md` and send it to Sage and Webby.
|
||||
2. Stop if something depends on it. If a reference shows a live dependency (the Console or another package
|
||||
imports code that only the control board has, or a unit or script still starts it), stop. Sage takes it to
|
||||
Mos before anything is removed.
|
||||
3. Otherwise retire the writer. Either remove the `serve` entry point, or make it refuse to start with a
|
||||
message that points to the Console. Keep any read-only code another package imports, and say which.
|
||||
Update the docs that tell people to start it. The AGENTS.md text about the control-board reply convention
|
||||
is a separate matter. Change it only if it tells someone to start 7331.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Nothing in the stack can start a writer on 7331. A test proves it: the entry point refuses (or is gone), no
|
||||
script or unit starts it, and a grep check finds no starter. Each change gets a mutant that brings the writer
|
||||
back and the test kills it.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files or `~/.mosaic`.
|
||||
- No request to any live port.
|
||||
- Single test files run freely. Suites need Sage's slot.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves INVENTORY.md, then the manifest. The control-board and webui package suites, the cli suite and
|
||||
every `scripts/test-*.sh` are green on Sage's gate, and the mutants are killed.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Brief: a second gate lane, two full gates at once on dragon-lin
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0977 item 6 ("put the design for a second gate lane on the list now")
|
||||
and the MVP push (mos-0974). Gitea #1695. Owner Herb (Codex gpt-6.1-sol, a new builder seat), reviewer
|
||||
Webby, gate owner Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/herb/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## Why
|
||||
|
||||
Every full run shares one slot today. A full gate (cli suite plus every `scripts/test-*.sh`) takes about 13
|
||||
minutes or more, reviewers' runs and mutants take the same slot, and with eight or more seats building for
|
||||
10-27 the slot is the bottleneck. The slot exists because concurrent runs broke each other. The busy check is
|
||||
`agents/sage/work/gate-lane/busy-now.sh` (a copy of Sage's), and the gate is
|
||||
`agents/sage/work/gate-lane/gate-template.sh` (a copy of Sage's template). The host has 16 cores and 62 GB.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Every shared resource two concurrent runs can collide on, found from the code and tests, not guessed:
|
||||
Docker compose project names and networks (`scripts/test-task.sh` and others), fixed TCP ports in tests,
|
||||
unix socket paths and their 107-byte limit under TMPDIR, the sandbox's private `/tmp`, shared caches,
|
||||
scratch and worktree paths, the queue lock, and any test that reads host-wide state (process lists,
|
||||
`pgrep`, `/proc`). For each: how a lane gets its own copy, or why it is safe to share.
|
||||
2. Timing. Which tests have time limits that a doubled CPU load could break, measured, and what changes (a
|
||||
bound, or a lane rule that keeps those suites apart).
|
||||
3. The lane mechanism: lane ids, a lock per lane that a run takes and releases even on a crash, the busy
|
||||
check that reports per lane, and how a seat asks for a lane. Sage still grants it.
|
||||
4. The gate template takes a lane id and isolates everything from point 1 by it.
|
||||
5. Proof: two full gates on different candidates at once, three times, with zero cross-failures, plus the
|
||||
wall time of each against a single-lane run. Any flake found is named with its cause.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev` or slot files. The proof runs use
|
||||
scratch roots only, and it needs Sage's grant of the whole slot while it runs.
|
||||
- Test code changes that only isolate a resource are in scope. A change to what a test checks is not, and
|
||||
goes to its owner.
|
||||
- Docker networks and containers a run makes are removed by the run (trap), and nothing else is touched.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, both singly and in the two-lane proof.
|
||||
Reference in New Issue
Block a user