Compare commits

..
Author SHA1 Message Date
jason.woltje 9ba31b5b3f agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420) 2026-10-20 15:20:37 -05:00
jason.woltje 9db578ff54 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199) 2026-10-20 15:09:59 -05:00
jason.woltje 8eb9ddd12c plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1) 2026-10-20 15:06:22 -05:00
jason.woltje ae3c36f248 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414) 2026-10-20 14:42:15 -05:00
jason.woltje a2af7ab495 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row 2026-10-20 14:41:03 -05:00
jason.woltje aa1bd209e3 gate-lane: row 189 gate timing table for row 197 (Herb) 2026-10-20 14:25:17 -05:00
jason.woltje ace27dc5eb row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed) 2026-10-20 14:21:56 -05:00
jason.woltje a4a42dbac2 plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192) 2026-10-20 14:18:08 -05:00
jason.woltje a31f1a0f8b plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019) 2026-10-20 14:17:03 -05:00
jason.woltje 992650232f plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir 2026-10-20 14:14:33 -05:00
21 changed files with 887 additions and 436 deletions

No files matched your search

+3
View File
@@ -22,6 +22,9 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| 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 |
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | 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` |
-292
View File
@@ -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.
-125
View File
@@ -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.
+9
View File
@@ -0,0 +1,9 @@
# Gizmoduck
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
access.json loader and validator, under Mos's order mos-1023 item 1.
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
@@ -0,0 +1,158 @@
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
# host access through the credential store
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
## What it changes
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
(Vikunja #420).
When this version lands, a reviewed commit:
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
within-role class (row 199 D9 step 1).
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
the push sends it by oid (row 199, duckworth-0022 N9).
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
file commit in `roles/`.
## Text for the PRD
Status line, appended to the current paragraph:
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
> and adds agent host access through the credential store, approved by Jason in decision
> <prd.approve decision id>.
Source, a new bullet above "Version 1.6 source":
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
> me. This will require sudo access to several systems. If we can utilize the credential store in
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
Under "Added in version 1.2", after REQ-COS-2:
> Added in version 1.7:
>
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
> under this requirement, within these bounds:
> - The chief acts only in its own organization.
> - The chief hires only role definitions that need no credentials, until a later version adds
> credentialed roles.
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
> change.
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
> WebUI or the CLI.
> - The kill switch stops every hire and fire while it is on.
> - Every hire and fire is logged with the chief's instance, its run and the reason.
Under "Landing", after REQ-LAND-1:
> Added in version 1.7:
>
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
> names that commit, and it doesn't authorize any other.
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
> name, the agent never does. A push never forces.
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
> and the host never contacts it. For each push the host builds the fork's address over https from
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
> the push and the credential match all use that address. So the agent's own user owns the receiving
> repository on every push, and the agent never names it.
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
> history. Gitea's push-to-create stays off, so a push never creates a repository.
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
> other role file grants it.
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
> the human resolved, and that decision named the commit.
>
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
Under "Credentials", after REQ-CRED-2:
> Added in version 1.7:
>
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
> for the host credential by name. The stack checks that a grant exists for that agent and that host
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
> grants a host credential, and how, is set by the #424 design, which Jason approves.
REQ-WAIT-3, appended:
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
## Notes for the reviewer and for Mos
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
sentence its actions and bounds. It doesn't widen REQ-COS-1.
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
named the refused authorities instead, which let merge-gate and any later action through.
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
holds through row 194's chain on its candidate.
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
bounds, so they stay out of the text (megavolt-0008).
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
the routing commit (duckworth-0022 N9, sage-1387).
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
(row 194 D2.3).
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
`hireable` list stays a reviewed commit in `roles/`.
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
version lifts it.
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
the #424 design picks one (duckworth-0038 B1).
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
paragraph never edited in place except to append.
+8
View File
@@ -0,0 +1,8 @@
# Megavolt
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
credential store per agent with its own git identity (#1697).
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 `megavolt/*`
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
+10
View File
@@ -0,0 +1,10 @@
# Quackerjack
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
section 7 (#414).
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
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
another seat's branch.
+36
View File
@@ -0,0 +1,36 @@
# Row 189 gate timing (for row 197, Herb)
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
| Suite | Wall s | duration_ms s | Ended (UTC) |
|---|---|---|---|
| pkg-bus | 3 | 3.8 | 19:09:40 |
| pkg-business | 3 | 2.2 | 19:09:43 |
| pkg-cli | 388 | 388.5 | 19:16:11 |
| pkg-control-board | 1 | 0.8 | 19:16:12 |
| pkg-conversation | 35 | 35.4 | 19:16:47 |
| pkg-discord | 3 | 2.6 | 19:16:50 |
| pkg-harness | 12 | 11.6 | 19:17:02 |
| pkg-ledger | 13 | 13.5 | 19:17:15 |
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
| pkg-plugins | 0 | 0.3 | 19:17:18 |
| pkg-project | 1 | 1.2 | 19:17:19 |
| pkg-runs | 1 | 0.8 | 19:17:20 |
| pkg-seat | 6 | 6.2 | 19:17:26 |
| pkg-tasks | 1 | 1.2 | 19:17:27 |
| pkg-webui | 58 | 57.9 | 19:18:25 |
| script-auth | 1 | - | 19:18:26 |
| script-conductor | 5 | - | 19:18:31 |
| script-config | 2 | - | 19:18:33 |
| script-discord | 8 | - | 19:18:41 |
| script-extension-package | 1 | - | 19:18:42 |
| script-foundation | 40 | - | 19:19:22 |
| script-land-work | 11 | - | 19:19:33 |
| script-queue | 21 | - | 19:19:54 |
| script-release | 11 | - | 19:20:05 |
| script-task | 31 | - | 19:20:36 |
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
+86
View File
@@ -0,0 +1,86 @@
# Row 189: `stack-release.sh activate --no-send`
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
brief commit on 9ae8c73ac).
## What it does
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
(exit 4).
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
refuses `already-active`.
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
- `send: dropped; --no-send was given`
- `send: off; --no-send was given and the pointer holds no --send`
- `send: carried` (the pointer's `--send` goes on unchanged)
- `send: off`
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
--send, so there is nothing to drop`.
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
own row.
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
backup sum equals the stopped store's, and the units stop and start in order.
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
and no drop line.
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
keeps it off.
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
## Mutants
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
plus T1 and T2.
- M1: the same-copy exception ignores `--no-send`.
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
drops it.
- M4: rollback re-adds `--send`.
- M5: the parser accepts `--no-send` for rollback.
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
## Deviations from the brief
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
:828).
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
## Outbound calls
None. The change edits an array before the existing unit render and pointer write. Every test uses
the stand-in systemctl, units, stores and data root the file already builds; no test touches
`~/.mosaic-dev`, the live units or a slot file.
@@ -0,0 +1,5 @@
M1 KILLED fail=5
M2 KILLED fail=2
M3 KILLED fail=7
M4 KILLED fail=2
M5 KILLED fail=2
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and
# the plan-tail tests, and restores the file from a checked backup.
set -uo pipefail
WT=/mnt/storage/scratch/sage-b189
OUT=/mnt/storage/scratch/sage-b189-mut/out
S=$WT/scripts/stack-release.sh
mkdir -p "$OUT" || exit 1
cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; }
ORIG=$(sha256sum "$S" | cut -d' ' -f1)
[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; }
restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; }
trap restore EXIT
mutate() {
case "$1" in
M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;;
M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;;
M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;;
M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;;
M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;;
esac
}
for m in M1 M2 M3 M4 M5; do
restore
mutate "$m"
if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi
bash -n "$S" || { echo "$m SYNTAX"; continue; }
diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff"
(cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1
fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log")
if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi
done
+9 -1
View File
@@ -368,7 +368,7 @@ reports checkout or copy identity and refactor drift, and writes nothing.
```
scripts/stack-release.sh adopt BUSINESS
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
scripts/stack-release.sh status BUSINESS
@@ -480,6 +480,14 @@ copies. It checks the file with `stat` and `realpath` and never reads the value.
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
and only the Console flags change.
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
Console flags and the unit, so the Console serves reads and no human sends. It
may name the live copy while the pointer holds `--send`; once `--send` is gone a
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
`--plan` prints one `send:` line: `dropped`, `off` with or without the
`--no-send` note, or `carried`.
#### A target past c216f47db: the PM after the harness kind
From c216f47db on, the bus launches the PM only with a `harness` kind in the
+6 -5
View File
@@ -1,7 +1,8 @@
# Brief: activate --no-send drops the Console's --send
Sage, 2026-10-20, for Mos's ruling mos-0972 (Q175 interim, Vikunja #412). Owner Sage, reviewer Dewey, gate owner
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit.
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. The candidate corrects them;
the first draft took them before row 163.
This row builds the tool only. The live restart without `--send` is a separate step afterwards, a same-copy
activate under Q173 with a backup first and a STATE report after. No test here touches `~/.mosaic-dev`, the
@@ -14,11 +15,11 @@ Mos ruled that the Console runs without `--send` until Q175 phase 2, because wit
process can forge `X-authentik-username` on 127.0.0.1:7332 and send as Jason (design section 8). On the pinned
stack, the Console flags live in the pointer (`~/.mosaic-dev/state/stack-active.json`):
- `console_for_target` (scripts/stack-release.sh:658-687) carries the pointer's flags unchanged. Only
- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only
`--tester-port` and the row 187 proof pair can change.
- `check_target` (:247-249) lets an activate name the pointer's copy only for a proof-pair change.
- `adopt` refuses once a pointer exists (:822).
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:640-651) refuse
- `check_target` (:249-256) lets an activate name the pointer's copy only for a proof-pair change.
- `adopt` refuses once a pointer exists (:828).
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:646-657) refuse
`unit-flags-drift` on every later activate, pm-on and rollback.
So the tool can't drop `--send` today. This row adds that one path.
@@ -0,0 +1,57 @@
# Brief: a credential store per agent, own git identity, pushes to a remote
Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan
(`jason-master-restart/projects/mosaic-mvp/PLAN.md` item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store
available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and
other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner
Sage. Base: refactor at the brief commit. Design first. The first deliverable is
`agents/megavolt/work/queue-ROW/DESIGN.md`.
This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any
code is written.
## What exists today (GAP-MAP, measured at 9ae8c73ac)
- G11. Credential references exist per instance for Gitea and Vikunja only
(`packages/business/src/credentials.mjs`, `packages/bus/src/credentials.mjs`, `vocabulary.mjs:103`). One
Claude env file is shared by every seat.
- G13. Agents use their own Vikunja token (`packages/tasks/src/adapter.mjs`). Git goes through the host
socket (`packages/cli/src/git-socket.mjs`). Gitea tokens are loaded but never used.
- The development repository's helper (`scripts/git-credential-mosaic.mjs`, wired in `.git/config` for
git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own `<seat>/*`
branches through it (mos-0981).
## What the design must settle
1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes,
ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS
equivalent"). A process asking for another agent's entry is refused and logged.
2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs
and which wait. The model provider entry must not copy jwoltje's `claude-session.env`. If it needs a new
provider account or subscription, flag it for Jason and don't pick one.
3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a
remote it is granted. Which refs an agent may push (its own branches, never the default branch or another
agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How
this fits the host git socket, or replaces it.
4. Lifecycle. Issue, rotate (`rotateBy`, `expires`), revoke and delete, and what row 194 (agent lifecycle) calls
when an agent is hired or fired. A fired agent's credentials stop working at once.
5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173
exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint
accounts.
6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push
rule ends only when Mos says so.
7. What this row closes in G11 and G13, and what stays open.
## Boundaries
- No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a
fake Gitea.
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, `~/.mosaic` or the live
`.git/config` of the canonical checkout.
- No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test.
## Gate
Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus,
business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's
mutants are killed.
@@ -0,0 +1,120 @@
# Federation build rows R1, R2 and R5 (#414)
Sage, 2026-10-20, for Mos's orders mos-1023 item 1, mos-1028 item 2 and mos-1034 item 1. The design is
Gizmoduck's `agents/gizmoduck/work/federation/DESIGN.md` at a1daa3c07 on `gizmoduck/federation-design` (r4.9,
sha256 a31cbebd070f9e3a5ca4ccb158b1747c6f5a83c3469855705b0026a581fb5c6a). Duckworth approved r4.5
(duckworth-0012), r4.6, r4.7 (duckworth-0020) and r4.8 (duckworth-0021, 8a22d0897 on `duckworth/reviews`),
and acked r4.9 (duckworth-0024). r4.7 applies Jason's Q181 hosts, r4.8 adds N22's R8 checks and r4.9 names who
runs them. None of them changes R1, R2 or R5. Section 7's table
sets each row's scope and test gate. Vikunja P19 #414. Seats push only their own `<seat>/*` branches
(mos-0981).
The other R rows are filed when their parents have candidates: R3 after R1 and row 196, R4 after R3, R5 and
row 193's viewer level (193a, mos-1019), R6 after #412 phase 1, R1, R2 and R3, R7 after R1-R6 and row 190.
R8 is a live window, not a code row. Its brief is the last section here.
## Brief: FED R1, the access.json loader and validator
Owner Gizmoduck, reviewer Duckworth, gate owner Sage. Base: refactor at this brief's commit. No parent.
`packages/business/src/access.mjs`: the access.json loader and validator from DESIGN.md section 3.5, and the
pure `covers`. Providers, peers and businesses come in as arguments. One read plus the file checks. The
exported interface is what R5 and row 193a build against, so the manifest names it, and any change to it in
review goes to Sage before it lands. Section 7 names `covers` but not the loader and validator exports or
the context shape. Morgana's 193a (508ee4d61 on `morgana/row193a`) assumes `checkAccess(doc, {providers,
peers, businesses})`, with `businesses` mapping an id to `{human, instances}`, throwing a `BusinessError` that
names the key path. Use those names, or tell Morgana and Quackerjack yours before the manifest goes to
Duckworth. Row 193a later widens the level check to `viewer` on R1's candidate (mos-1019).
Tests, in `packages/business/tests/`: every rule in 3.5 refuses with the key path named. That covers a
symlink, a non-regular file, another owner, group or other write, expiry, `*`, a grant disjoint from the given
subjects, `human` and `tester` ids, any level other than `collaborator`, and a project entry that isn't a
UUID. For `covers`: `*` and listed ids match, while an unlisted project, instance or business doesn't, an
expired grant doesn't, and a level filter rejects another level. One mutant per refusal, each killed.
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. Tests
use scratch roots. Single test files run freely, and suites need Sage's slot.
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
## Brief: FED R5, mosaic access list, grant and revoke
Owner Quackerjack, reviewer Duckworth, gate owner Sage. Chained on R1 (Q177). The build starts now against
R1's exported interface as the R1 manifest names it, and R5 lands after R1 and rebases onto R1's landing.
`mosaic access list`, `grant` and `revoke`: human proof, atomic write, revision bump, a receipt line. The help
text states the `instances` limit and the provider-removal order (DESIGN.md section 7, row R5). R4 later makes
these verbs call `access.reload`, so R5 leaves no other reload path.
Context (sage-1419). `ssoSubjects` is row 192's `console.sso.providers[].subjects` flattened to `{provider,
sub}` pairs; the design's "`console.sso.subjects`" in 3.5 is shorthand for it. `providers` is the list of
`providers[].id`, read through `loadSystem` and row 192's `checkConsole`. `peers` is `[]` until R2 lands, so a grant
naming a peer home refuses. R5 rebases onto row 192's landing as well as R1's.
Tests, in `packages/cli/tests/`: tester and agent callers are refused, a concurrent write refuses on a
revision conflict, the receipt line is appended, and a bad grant is refused before anything is written. One
mutant per refusal, each killed.
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. The
human proof in tests uses the scratch stand-in pattern (a scratch bus verifies its own human-cli path).
Single test files run freely, and suites need Sage's slot.
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: FED R2, the federation key in the system config
Owner Quackerjack, reviewer Webby, gate owner Sage. Chained on row 192 (#412 phase 1). The build starts when
row 192 has a candidate, on that candidate.
A `federation` key in `scripts/mosaic-config.mjs` with the shape in DESIGN.md section 3. A config with no
`federation` key still loads as it does today. This changes the config schema, so the manifest lists every
reader of the key.
Tests, in `scripts/test-config.sh`: a valid key loads, an unknown sub-key and a bad origin refuse, and a
config without the key loads. One mutant per refusal, each killed.
Boundaries: `~/.config/mosaic-dev/config.json` is user-authored and is not touched. Tests use scratch config
dirs only.
Gate: Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and
the mutants are killed.
## Brief: FED R8, the hub window on 10.1.1.54
A live window, not a code row (DESIGN.md section 7, row R8). The brief follows r4.11, b93f098f0 on
`gizmoduck/federation-design` (sha256 1bcee8df0a3a8628a6646535b756c936ca6d9e7bcfa7e1dd2bf1d3f01715c8e6),
which Duckworth acked with r4.10 (duckworth-0034). Mos closed design rounds there (mos-1072). Run by Jason and
Mos. Nothing here authorizes a step on any host; each step waits for the window's go.
What changed since r4.9 (DECISIONS Q182, Q183, mos-1055, mos-1063). R8 is a recorded acceptance. It starts
with Jason's acceptance of the factory paths to .54 on record, quoted in section 8. The privilege and host
checks still run, and each result goes into the R8 record with the name of whoever ran it. They report and
don't refuse. The four functional checks named at the end of section 7's R8 row still stop R8 when they fail
(mos-1066 item 1).
Order (mos-1042 item 2). R8 runs after the Case 2 install on .54 (#416, due by 2026-10-27, Jason's latest-by
date in Q176, mos-1069), after Bean and Chet are live in Case 2, and after both Hermes gateways on 10.1.1.54
have stopped. Case 2 go-live doesn't wait on R8. The risk, stated for 10-27: federation between the two
instances depends on Melanie's switch-over date.
Entry checks, all before anything starts:
- R7, #412 phase 2 and Jason's #419 answer, as section 7 lists.
- The four hub user checks on 10.1.1.54 (N22, mos-1035 item 1): `sudo -l -U mosaic` grants nothing, `id
mosaic` shows only its own group, `~mosaic/.ssh/authorized_keys` is absent or empty, and no file under
`/etc/sudoers.d` names `mosaic`. An agent or Jason runs them as localadmin (mos-1055), and the output goes
into the R8 record with who ran them.
- The factory privilege checks, the key fingerprint comparison and the section 8 privilege path table, each
as section 7's R8 row states them.
Build notes:
- N1 (Duckworth, duckworth-0034, mos-1072). A check that fails on a path Jason didn't name in Q182 or Q183 is
marked "not named in Q182 or Q183" in the record, not filed under the acceptance. Examples: a factory mount
of hub data, another agent on .54, a provider admin token. Mos takes each one to Jason.
- N2 (Duckworth, doc-only). The Q183 and Q4 quotes in section 8 are excerpts. When the R8 record quotes them,
it marks each omission with "…" and checks the full text at DECISIONS.md:1535-1541 in jason-master-restart
(read only).
Done when every entry check has its output recorded, Jason signs in to the hub and sends one message, Melanie
revokes, and his next request is refused.
+70
View File
@@ -0,0 +1,70 @@
# Brief: agent host access with sudo through the credential store (#424)
Sage, 2026-10-20, for Jason's rulings Q183 and Q184 relayed by Mos (mos-1056 item 3, mos-1067). Gitea #1707, Vikunja P19
#424, due 2026-10-27 (Q184: "Q5: by 10.27"). Owner Gizmoduck (design and build), reviewer Duckworth, gate owner
Sage. Chained on row 199's candidate (Q177): row 199 design r6 (4d3007ec9 on `megavolt/row199`, approved in
duckworth-0027) is not reopened. This row lands after 199 and rebases onto its landing.
## What Jason asked for
Q183, through Mos: "I envision a credential store within Mosaic Stack that works as some sort of variable for
the agent to call. The system will enforce access to the credentials to keep the agents sandboxed with proper
security... If we can utilize the credential store in Mosaic Stack and allow agents to perform their jobs with
NOPASSWD sudo on various machines, I am a happy man." Q183 also says not every agent needs that access.
Mos's requirements (mos-1067 item 1):
- An agent asks for a host credential by name.
- The stack checks a grant for that agent and that host, and does the ssh and sudo for it.
- The agent never sees the key or any secret value.
- Only agents with a grant can do it.
The PRD text is REQ-CRED-3, drafted by Honker for PRD 1.7 (sage-1422). If it misses 1.7 it goes into 1.8, and
this row lands only after the version that holds it is approved.
## What row 199 gives and what it doesn't (megavolt-0019)
It builds on 199 as 199 stands: the store layout under `<configDir>/secrets/agents/<business>/<agent>/`, the
lifecycle verbs and their `changes.jsonl` receipts, `useFor(session, service, fn)` with the D5 allowlist test,
`assertClean` on every result, and the grant shape of `publish` (a list on the role entry, checked by the
broker, plus a hard-wait action that isn't routed).
This row's own changes:
- A host kind in the closed kind set (vocabulary.mjs `SERVICES`, credentials.mjs, the broker's service check,
the CLI date flag, the remove regex), with a naming rule for one credential per host or per agent.
- A per-kind value check. An OpenSSH private key fails today's `TOKEN` pattern.
- Handing the key to ssh without the agent seeing it: an ssh-agent on a private socket, or a 0600 temp file
removed on every path, the way publish's one-shot helper works.
- Pinned host keys per host, and no trust on first use.
- The command boundary: what an agent may run under sudo, and how output returns without echoing the secret.
This is the main design work.
- The executor: a ticket and a host op running in the bus child, the same split row 199 uses for publish
(Megavolt's deviation in 199's BUILD.md).
## Steps
1. DESIGN.md first, in `agents/gizmoduck/work/queue-ROW/`. It answers each item above, names the grant shape
(`roles.<agent>.hosts` and a `host.sudo` action, or a reasoned alternative), says who grants (Jason, through
a reviewed role change, unless REQ-CRED-3 says otherwise), and lists tests and mutants. Duckworth approves it
before code. Sage reads it too, because it gives agents sudo on hosts.
2. Build on 199's candidate. Single test files run freely. Suites and mutant batches need Sage's slot.
3. The live part is not this row. Putting a key in the store and granting a host is a separate window with
Jason or Mos, after landing.
## Hosts
The first targets are `localadmin` with NOPASSWD sudo on 10.1.1.53 and 10.1.1.54 (#423 step 1, applied by
ops-01 at 20:05:47Z), and the servers agents already reach as localadmin (Q172). ops-01 owns the host side.
Tests use scratch sshd fixtures on 127.0.0.1 or fakes, never a real host.
## Boundaries
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files, the live units or `~/.mosaic`. No key is
read, copied or printed. No ssh to any real host from a test or a seat.
- No widening of row 199's kinds or regex inside row 199. Both rows touch them, so this row rebases after 199
lands.
## Gate
Duckworth approves DESIGN.md, then the manifest. The bus, business and cli suites and every
`scripts/test-*.sh` are green on Sage's gate, and the mutants are killed (one per refusal, plus one that leaks
the key into the agent's output and one that skips the host key pin).
@@ -0,0 +1,70 @@
# Row 193 build rows: 193a, 193b1, 193b2, 193c and 193d (#1691)
Sage, 2026-10-20, for Mos's go mos-1034 item 4. Row 193's design is Morgana's
`agents/morgana/work/queue-193/DESIGN.md` at e2d161e2e on `morgana/row193` (r5, sha256
dd7fcb419ed8510e9d8d74b569183386d0942af8b68eecc0bbb5e182e677b523), approved by Duckworth (duckworth-0018).
Section 8 lists the tests and mutants, and section 9 the split. The brief behind them is
`docs/plans/2026-10-20_human-users.md`. Vikunja P19 #414/#415, Gitea #1691.
Owner Morgana for every row, reviewer Duckworth, gate owner Sage. Each row chains on its parent's candidate
(Q177) and rebases onto the parent's landing. Seats push only their own `<seat>/*` branches (mos-0981).
Single test files run freely, and suites and mutant batches need Sage's slot.
Boundaries for every row: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, any
live access.json or `~/.mosaic`. No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
No user thread goes live before 193b1's message filter has landed (mos-1021 B2, FED R8).
The owner-visible records are fixed by Mos (mos-1023 item 2) and named in one user sentence, which FED R6
reuses word for word: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks
and project files, but not your messages."
## Brief: 193a, the access level check widened to viewer
Chained on FED R1. Started early on `morgana/row193a` (508ee4d61) against R1's interface as the FED brief
names it, with Sage's go (sage-1373). R1's level check accepts `viewer` and still refuses any other new
level. Tests in `packages/business/tests/` (section 8, Business), with the four mutants in Morgana's
BUILD.md. FED R4 chains on this row.
Duckworth's N1 on R1 (duckworth-0033, sage-1417): with viewer added, `covers` called with no level would let a
viewer grant cover a collaborator ask. This row makes `level` required in `covers`, with a test and a mutant that
makes it optional again. R3 and R4 call `covers` with a level.
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
## Brief: 193b1, viewer verbs, board refusals and the message filter
Chained on FED R3, FED R4 and row 196's verifier. The bus gets the viewer verb list, per-grant level
checks, board reads refused to users, and the section 5.7 `trail` and `replies` filter with `#readable`
owner-only. No schema change. The governance pin test names each item in the user sentence above. Bus tests
and mutants 1, 3, 6, 10, 11 and 12 from section 8.
Gate: Duckworth approves the manifest. The bus suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: 193b2, thread shares (schema 3j)
Chained on 193b1. `thread_shares`, the share verbs, the share rules of `#readable` and `#threadRead`, and the
section 5.5 share filters. Schema 3i to 3j. The table is append-only, and row 181's restore guard and
`COUNTED` include it. Bus tests for shares and the migration, and mutants 2, 4, 7, 8 and 9 from section 8.
Going live needs a `--migrate` window with Jason. Sage takes it to Mos when this row is near.
Gate: Duckworth approves the manifest. The bus suite, the cli suite and every `scripts/test-*.sh` are green on
Sage's gate, and the mutants are killed.
## Brief: 193c, mosaic access user verbs and --level viewer
Chained on FED R5. `mosaic access user add/remove` and `grant --level viewer`, only where R5 lacks them. They
refuse tester and agent callers, write a receipt line and call `access.reload` (section 8, CLI). If R5
already has all of them, this row records that and closes with no code.
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: 193d, the Console's shared list, share controls and viewer read-only
Chained on FED R6, row 192 and 193b2 (for share controls). A viewer has no composer or share control, and
with each UI check removed the bus still refuses (section 8, Webui).
Gate: Duckworth approves the manifest. The webui suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
+17 -2
View File
@@ -24,11 +24,26 @@ on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least.
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.
## Mos's call on the Console dependency (mos-1019)
The inventory (004f133bd) found that the Console proxies `/api/board`, `/api/seen`, `/api/reply` and the
conversation routes to `DEFAULT_BOARD` on 7331 (`packages/webui/src/serve.mjs:12`), so the board view returns
502 today. Mos took Sage's recommendation: retire the 7331 listener and its README start commands, and have
the Console call the board's read code in-process, behind the Console's own auth (Q175 SSO on 7332).
`/api/reply` stays only under `--send`. The DESIGN.md for that change comes before code, and Webby approves
it.
## 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.
back and the test kills it. Mos added three items (mos-1019):
- The board view answers from the Console with nothing listening on 7331.
- Every board route sits behind the Console's auth.
- A test proves `/api/reply` refuses without `--send`.
Test fixtures that bind port 0 may stay if they only test the code in-process.
## Boundaries
@@ -38,5 +53,5 @@ back and the test kills it.
## Gate
Webby approves INVENTORY.md, then the manifest. The control-board and webui package suites, the cli suite and
Webby approves INVENTORY.md, then DESIGN.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.
@@ -0,0 +1,39 @@
# Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite)
Sage, 2026-10-20, for Launchpad's finding F3 on row 192 (launchpad-0001) and Q175 (#412). Gitea #1698. Owner
Launchpad, reviewer Webby, gate owner Sage. Chained on row 192's candidate (Q177): it lands after row 192 and
before the phase 2 window. Base: row 192's candidate.
## Why
The phase 2 window re-pins the Console to the phase 1 commit with `--sso --send --tester-port 7333`
(`agents/sage/work/q175-sso/DESIGN.md`, section 5). `scripts/stack-release.sh` doesn't know `--sso` today.
Its `proof_checks` (`:751`) requires `--public-host`, `--auth-header` and `--allow-user` whenever a proof pair is
carried, and the pointer's Console flags still hold the forward-auth pair (`--auth-header X-authentik-username
--allow-user jason.woltje`) that `--sso` refuses to start with. So the window can't run through the script, and
a hand-edited pointer is not an option.
## What the row does
1. `activate ID BUSINESS --sso` writes Console flags with `--sso` and without `--auth-header`, `--allow-user`
and any proof pair, and `--plan` names each flag it drops and carries, in the style of row 189's `send:`
line. The design names the full flag set the phase 2 Console starts with and checks it against row 192's
refusals.
2. Before anything stops, the script refuses a target copy whose Console doesn't support `--sso`, and refuses
when the config's `console` key or its slot file fails the checks row 192's `--sso` start runs. It reads
neither value and only uses the same stat and shape checks.
3. Rollback from an `--sso` pointer to a pre-phase-1 copy follows the design's phase 2 rollback order (remove
the `console` key first). The script refuses a rollback that would start a Console on a config it can't
read, and says which step comes first.
4. `docs/TOOLS.md` gets the flag.
## Boundaries
- No change to the live pointer, units, `~/.mosaic-dev` or `~/.config/mosaic-dev`. Tests use scratch roots
and seeded pointers, as rows 187 and 189 do.
- No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
## Gate
Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and the
mutants are killed (one per refusal, and one that keeps `--auth-header` in the written flags).
+112 -5
View File
@@ -381,7 +381,7 @@ async function seed(e,{copy=B,pm=false,store='3g',pointer:how='adopt'}={}){
const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`;
const planBackup=(e)=>`backup: the run backs up the stopped store to ${e.data}/backups/stack/<activation>/ before any migration`;
const planNoBackup='backup: none; the run rolls back before the backup';
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off','send: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
// --- The T table ---
@@ -1030,17 +1030,17 @@ test('T36 --plan shows the pending state and lists the row 169 refusals it can s
return r.out.split('\n');
};
let out=await quiet(0,'activate',C1,'acme','--pm-after-harness','--plan');
assert.deepEqual(out.slice(-6),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
biz(e,HARNESS);
out=await quiet(3,'activate',A,'acme','--pm-after-harness','--plan');
assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
assert.deepEqual(out.slice(-8),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
out=await quiet(3,'activate',C1,'acme','--pm-after-harness','--plan');
assert.equal(out.at(-2),'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm');
biz(e);assert.equal((await run(e,'activate',C1,'acme','--pm-after-harness')).code,0);
out=await quiet(3,'activate',C,'acme','--plan');
assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
assert.deepEqual(out.slice(-8),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
out=await quiet(0,'rollback','acme','--plan');
assert.deepEqual(out.slice(-6),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
});
// Row 176 test 11: supported and older Console copies deliberately differ from the checkout.
@@ -1706,6 +1706,113 @@ test('T54 row 188: a carried pair gets the proof file checks, and a proof file i
});
});
// Row 189 (#1687, mos-0972/0973): activate --no-send drops the pointer's --send; nothing turns it back on.
const sendLines=(out)=>out.filter((l)=>l.startsWith('send:'));
const noSend=(c)=>c.filter((x)=>x!=='--send');
const LIVE_NOTE=`note: ${B} is the pointer's copy; the run re-pins it and only the Console flags change`;
test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{
await t.test('test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);
assert.deepEqual(want.slice(-2),['--tester-port','7333']);
const sum=createHash('sha256').update(readFileSync(e.store)).digest('hex'),n=calls(e).length;
let r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,want);
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));assert.doesNotMatch(conExec(e),/ --send( |$)/);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
const act=pointer(e).activation,l=logLines(e).at(-1);
assert.deepEqual([l.op,l.from,l.to,l.result,l.backup],['activate',B,B,'ok',act]);assert.equal(backupSum(e,act),sum);
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
await noLeak(e);
});
await t.test('test 2: a plain live-copy activate still refuses already-active, and so does --no-send once --send is gone',async(t)=>{
const e=await mk(t);await seedPreview(e,undefined,['--send']);
await refused(e,['activate',B,'acme'],'already-active',`${B} is the pointer's copy`);
await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);
await refused(e,['activate',B,'acme','--no-proof'],'already-active',`${B} is the pointer's copy`);
const r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
await refused(e,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`);
const g=await mk(t);await seedPreview(g);
await refused(g,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`);
for(const x of [e,g])await noLeak(x);
});
await t.test('test 3: --plan prints one send: line in each case, with the live-copy note on a same-copy run',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);
let out=await planOnly(e,0,'activate',B,'acme','--no-send');
assert.deepEqual(sendLines(out),['send: dropped; --no-send was given']);
assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[LIVE_NOTE]);
assert.equal(out[out.indexOf('send: dropped; --no-send was given')+1],`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${noSend(base).join(' ')}`);
out=await planOnly(e,0,'activate',C1,'acme');
assert.deepEqual(sendLines(out),['send: carried']);assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]);
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(' --send'));
assert.deepEqual(sendLines(await planOnly(e,0,'activate',C1,'acme','--no-send')),['send: dropped; --no-send was given']);
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,noSend(base));assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme','--no-send')),['send: off; --no-send was given and the pointer holds no --send']);
assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme')),['send: off']);
assert.deepEqual(sendLines(await planOnly(e,0,'rollback','acme')),['send: off']);
r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.match(stepText(e),/Z --no-send was given and the pointer holds no --send, so there is nothing to drop\n/);
assert.doesNotMatch(stepText(e),/drop --send/);
await noLeak(e);
});
await t.test('test 4: after --no-send, a rollback to the earlier copy leaves --send out of the pointer and the unit',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/);
assert.doesNotMatch(stepText(e),/--send/);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
await noLeak(e);
});
await t.test('test 4: pm-on after --no-send keeps --send off',async(t)=>{
const e=await mk(t),base=await seedPreview(e,{pm:true},['--send']);
let r=await prun(e,'activate',C1,'acme','--pm-after-harness','--no-send');assert.equal(r.code,0,r.out+r.err);
biz(e,HARNESS);
r=await prun(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).pm,true);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
await noLeak(e);
});
await t.test('test 5: --no-send on rollback, pm-on, status, adopt or rehearse is a usage error',async(t)=>{
const e=await mk(t);await seedPreview(e,undefined,['--send']);
const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e);
for(const args of [['rollback','acme','--no-send'],['rollback','acme','--plan','--no-send'],['pm-on','acme','--no-send'],['status','acme','--no-send'],['adopt','acme','--no-send'],
['rehearse','acme','--backup','/tmp/x','--port','7400','--no-send']]){
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
}
assert.match(usageText,/\| --no-proof\] \[--no-send\]/);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await noLeak(e);
});
await t.test('test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),pre=noSend(base),f=await proofFile(e),g=await proofFile(e,'proof2.189',0o400);
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send']);
let out=await planOnly(e,0,'activate',C1,'acme','--no-proof');
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: carried']);
out=await planOnly(e,0,'activate',C1,'acme','--no-send');
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: carried, header x-console-proof','send: dropped; --no-send was given']);
out=await planOnly(e,0,'activate',B,'acme','--no-send','--no-proof');
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: dropped; --no-send was given']);
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(f)]);
r=await prun(e,'activate',C1,'acme','--no-send',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two')]);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
const h=await mk(t),hbase=await seedPreview(h,undefined,['--send']),hpre=noSend(hbase),k=await proofFile(h);
r=await prun(h,'activate',B,'acme',...pair(k));assert.equal(r.code,0,r.out+r.err);
r=await prun(h,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(h).console,[...hpre,'--send']);
r=await prun(h,'activate',C1,'acme','--no-send','--no-proof');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(h).console,hpre);assert.doesNotMatch(conExec(h),/--proof-| --send( |$)/);
const steps=stepText(h);
assert.match(steps,/Z --no-proof was given and the pointer holds no proof pair, so there is nothing to drop\n/);assert.match(steps,/Z drop --send; --no-send was given\n/);
r=await prun(h,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
for(const x of [e,h])await noLeak(x);
});
});
// --- rehearse (D2, row 163; the brief's "Stack rehearsal on a scratch root") ---
// Copies of their own in RCOPIES: the real scripts/mosaic, config loader, business loader, roles and
// mosaic-config.mjs, with stand-ins for the bus host (cli.mjs), the Console, the Store and
+38 -6
View File
@@ -3,7 +3,7 @@
# Moves the bus, the Console and the human CLI from the pointer's copy to another copy together,
# behind a health gate, and puts the old stack back inside the same run when the gate fails.
# Console tester listener flags follow target support (row 176, #1674); so does the row 178 proof
# pair, which activate sets or drops (row 187, #1685).
# pair, which activate sets or drops (row 187, #1685). activate --no-send drops --send (row 189, #1687).
# Design and tests: agents/darkwing/work/queue-162/BUILD.md.
#
# Who runs it. Decision 96 (Mos, mos-0716) lets this script stop and start the live units under
@@ -30,13 +30,13 @@ BIN="$HOME/.npm-global/bin"
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
CONSOLE_UNIT=mosaic-webui.service
TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT=""
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 R_BACKUP="" R_PORT=""
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 R_BACKUP="" R_PORT=""
usage() {
cat >&2 <<'USAGE'
usage: stack-release.sh adopt BUSINESS
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
stack-release.sh pm-on BUSINESS [--timeout S]
stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
stack-release.sh status BUSINESS
@@ -85,6 +85,7 @@ parse_opts() {
--proof-header) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_HEADER="$2"; shift 2 ;;
--proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;;
--no-proof) [ "$OP" = activate ] || usage; NO_PROOF=1; shift ;;
--no-send) [ "$OP" = activate ] || usage; NO_SEND=1; shift ;;
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;;
--backup) [ "$OP" = rehearse ] && [ $# -ge 2 ] && { [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || [[ "$2" =~ ^/[A-Za-z0-9/._+-]*$ ]]; } || usage; R_BACKUP="$2"; shift 2 ;;
@@ -244,14 +245,16 @@ target_schema() {
in_chain() { local v; for v in "${T_CHAIN[@]}"; do [ "$v" != "$1" ] || return 0; done; return 1; }
# The item 3 checks on the target copy, in order: copy, schema, already-active. An activate that
# sets a proof pair, or drops the pointer's pair with --no-proof, may name the pointer's copy: it
# re-pins the live copy with new Console flags (row 187, Part B's B3 and B-R1).
# sets a proof pair, drops the pointer's pair with --no-proof, or drops the pointer's --send with
# --no-send, may name the pointer's copy: it re-pins the live copy with new Console flags (row 187,
# Part B's B3 and B-R1; row 189, mos-0972).
check_target() {
copy_exists "$TO" 2>/dev/null || refuse copy-missing "no copy $TO in $COPIES"
copy_ok "$TO" 2>/dev/null || refuse copy-unreachable "$TO is not reachable from refactor and origin/refactor"
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION and its chain from $COPIES/$TO/packages/bus/src/store.mjs"
[ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] ||
{ [ "$NO_PROOF" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --proof-header "* ]]; } ||
{ [ "$NO_SEND" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --send "* ]]; } ||
refuse already-active "$TO is the pointer's copy"
}
@@ -690,6 +693,33 @@ console_for_target() {
DROP_TESTER="--tester-port $inherited"
fi
proof_for_target
send_for_target
}
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose
# it; without the flag the pointer's --send goes on unchanged. Rollback and pm-on never take it, so
# they carry the pointer as it is. Sets SEND_PLAN (the plan's send: line) and SEND_STEP.
send_for_target() {
local had=0 i
local -a without_send=()
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
if [ "${RUN_CONSOLE[i]}" = --send ]; then had=1; else without_send+=("${RUN_CONSOLE[i]}"); fi
done
SEND_STEP=""
if [ "$NO_SEND" = 1 ]; then
RUN_CONSOLE=("${without_send[@]}")
if [ "$had" = 1 ]; then
SEND_PLAN="send: dropped; --no-send was given"
SEND_STEP="drop --send; --no-send was given"
else
SEND_PLAN="send: off; --no-send was given and the pointer holds no --send"
SEND_STEP="--no-send was given and the pointer holds no --send, so there is nothing to drop"
fi
elif [ "$had" = 1 ]; then
SEND_PLAN="send: carried"
else
SEND_PLAN="send: off"
fi
}
# Row 187: the row 178 proof pair in RUN_CONSOLE. A new pair replaces the pointer's and goes last;
@@ -943,6 +973,7 @@ plan() {
echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$RUN_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE"
[ -z "$DROP_TESTER" ] || echo "drop $DROP_TESTER; the target Console does not support it"
echo "$PROOF_PLAN"
echo "$SEND_PLAN"
echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}"
echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN"
local r
@@ -956,6 +987,7 @@ run_steps() {
capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped"
[ -z "$DROP_TESTER" ] || say "drop $DROP_TESTER; the target Console does not support it"
[ -z "$PROOF_STEP" ] || say "$PROOF_STEP"
[ -z "$SEND_STEP" ] || say "$SEND_STEP"
trap 'after_fail signal' INT TERM HUP
stop_both || rollback_in_run stop-timeout
if [ -e "$STORE" ]; then
@@ -1478,7 +1510,7 @@ STORE="$DATA/bus/bus.sqlite"
BUS_UNIT="mosaic-bus@$BUSINESS.service"
FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL=""
STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED=""
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" PLAN_REFUSE=()
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" SEND_PLAN="" SEND_STEP="" PLAN_REFUSE=()
R="" R_RC=0 R_EARLY=() R_BK="" R_HOST_PID="" R_CONSOLE_PID="" R_STARTED="" LIVE_CFG="" LIVE_BIZ=""
case "$OP" in
adopt) do_adopt ;;