diff --git a/agents/bushroot/work/queue-198/DESIGN.md b/agents/bushroot/work/queue-198/DESIGN.md new file mode 100644 index 000000000..56761d760 --- /dev/null +++ b/agents/bushroot/work/queue-198/DESIGN.md @@ -0,0 +1,155 @@ +# 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). + +Note on base: brief commit a31f1a0f8 is local to the canonical checkout and is not on origin. This branch +does not contain it. I read the brief from that commit with `git show`. Sage, please push a31f1a0f8 or +confirm that the brief text is what the reviewers read. + +## 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. +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`.** Today the loopback Console forwards both POSTs with no + `--send` check. That is a real gap, and this row closes it. `/api/reply` runs `agent-send.sh` from the Console's + process, so it is a write like the other `--send` writes. I also gate `/api/seen`, which writes board marks under + the data root. Mos's ruling did not name `/api/seen`. I chose to gate it, and Sage should confirm. +6. **The live unit's `--board` flag goes.** The installed unit passes `--board http://127.0.0.1:7331`. With the + flag refused, the next restart from the old unit exits 2. The unit restarts only on exit codes other than 2 + (`RestartPreventExitStatus=2`), so it stays down. The re-render and restart are a human step (Jason or Mos). + This row changes the template and the script. It does not touch the installed unit. + +## Open points for Sage and Mos + +- **Auth on loopback.** Mos's item says "every board route sits behind the Console's auth." 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 at all. Today `/api/board` is open to any + local process in that mode, and so is every other Console read. This design makes the board routes follow + the same rule as every other read. If Mos wants loopback board reads gated too, that is a Console-wide + change and needs a ruling. I will not invent one. +- **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 Console restart is a human step (`console-holds-human-authority`). It needs the re-rendered + unit without `--board`. The live unit stays up until then. + +## 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) }`. Each method is the body of a route that `createServer` runs now. No `node:http`, no listener. | +| `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. 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. | +| `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`. | + +## 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) | + +Mutant runs use single test files, which run freely. Any suite (control-board, webui, cli, every +`scripts/test-*.sh`) needs Sage's slot. + +## Gate + +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-M9 killed. + +## Stop points + +- If Mos rules that loopback board reads need a gate, this design changes. 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.