304 lines
20 KiB
Markdown
304 lines
20 KiB
Markdown
# Row 164 / #1661 — channel kind design
|
||
|
||
Design only, Rocko, 2026-10-18. Brief: `docs/plans/2026-10-18_channel-kind.md`,
|
||
commit `c682d5f7a`, pinned blob `c20a2a19b99b`; Sage sage-0758.
|
||
Sage and Mos accepted the original design with sage-0766 conditions (sage-0769,
|
||
mos-0768). Sage approved the additive snapshot getter and chat-http placement in sage-0770. Row 164 is unclaimed:
|
||
`queue next rocko` says to wait for row 129. No tests or live reads/actions run.
|
||
|
||
Evidence below is from canonical HEAD `c682d5f7a`, except the explicitly marked
|
||
row-129 candidate. That candidate is frozen in
|
||
`agents/rocko/work/queue-129/r1/build.patch`, candidate manifest SHA-256
|
||
`cfbadbd49902f5f3740392fec9218279adc2a661cfb28341fba02790f327232b`.
|
||
Its line references were read in `/mnt/storage/src/mosaic-stack-rocko-129-r1/`.
|
||
Recheck them against the landed row-129 tree before implementation.
|
||
|
||
## 1. Interface, switch and call sites
|
||
|
||
Recommend registry entry `channel`, `interfaceVersion: 1`, `switch: "list"`,
|
||
required function export `dispatch`. Each module exports:
|
||
|
||
```js
|
||
export const interfaceVersion = 1;
|
||
export async function dispatch({ verb, args }, { call }) { /* ... */ }
|
||
```
|
||
|
||
`call(verb, args)` is a core-created, already business-bound human transport.
|
||
The module returns its resolved bus result unchanged, or propagates its refusal
|
||
unchanged. It receives no capability, proof, human/tenant selector, socket path,
|
||
process API or configuration writer. It must not import a transport or start a
|
||
process. The shipped modules only validate their supported operation and forward
|
||
it to `call`; presentation, HTTP guards, thread resolution and paging remain in
|
||
their current core surfaces. These are trusted in-tree modules, not a sandbox or
|
||
an authorization boundary (`loader.mjs:1`, `:41`; PRD `docs/prd/mosaic-stack.md:598`).
|
||
|
||
Supported operations are `project.list`, `project.create`, `thread.list`,
|
||
`thread.create`, `thread.messages`, `thread.send` and `thread.open`. The CLI
|
||
module also forwards `message.receive` and `message.read` for the existing
|
||
thread talk/follow loop only. These exports support future surfaces without
|
||
requiring surface-specific function names in the kind. An unsupported verb
|
||
refuses `channel-operation`; no arbitrary verb fallback. Existing REQUEST
|
||
construction stays in CLI `cli.mjs:354` and row-129 `chat.mjs:11`.
|
||
|
||
Core helper, new `packages/plugins/src/channel.mjs`:
|
||
|
||
```js
|
||
createChannelCaller({ name, call, log }) // returns async (verb, args = {}) => result
|
||
```
|
||
|
||
It reads current settings with the existing core `call('plugins', {})`, validates
|
||
that response and projects just its channel key into `createLoader` (or `{}`
|
||
when absent), then selects the requested name from
|
||
the matching `loaded().channel` / `modules('channel')` snapshot, and invokes
|
||
`dispatch`. The name is a core literal (`cli` or `webui`), never browser input.
|
||
Take names and modules as one atomic snapshot, then release selection before
|
||
invoking dispatch. Never hold a lock over dispatch: a slow thread.send must not
|
||
block project.list (sage-0766). The existing loader has only separate synchronous
|
||
getters, `loaded()` and `modules(kind)` (`loader.mjs:148`–`:157`); it does not
|
||
currently return matched pairs in one call. Proposed for Sage/Mos to choose:
|
||
add synchronous `snapshot(kind)` returning the current immutable list of
|
||
`{ name, module }` entries. Those entries and their array are already frozen
|
||
(`loader.mjs:71`, `:136`). The getter performs no await or callbacks. The helper
|
||
captures that one return value, selects the requested entry synchronously, and
|
||
invokes dispatch without a lock. No edits to path, version or last-good behavior.
|
||
Sage approved this additive-only getter in sage-0770. loaded(), modules(), path
|
||
checks, version checks and last-good stay byte-identical. Test pairing against
|
||
both existing getters, frozen return value, and captured-array stability after
|
||
a reload. This adds src/loader.mjs and its snapshot tests to the owned scope.
|
||
Keep a loader per caller; a malformed settings response refuses rather than
|
||
silently inventing configuration. Read settings again for each chat operation,
|
||
including the thread receive loop, so a valid off switch takes effect on the
|
||
next operation. An operation already admitted may finish once. A loader refusal
|
||
uses the existing last-good policy; first-load refusal has no enabled modules.
|
||
Log refusal metadata without message bodies or proofs.
|
||
Projecting the channel key avoids loading unrelated usage/search modules into
|
||
the human CLI or Console; it does not change the organization document.
|
||
|
||
| Surface | Existing seam | Proposed routing |
|
||
|---|---|---|
|
||
| CLI project create/list | `cli.mjs:541` | Pass a channel caller into `projectBusCommand`; local init/check remain core. |
|
||
| CLI threads and thread create/read/open | `cli.mjs:542`, `:543`; `thread.mjs:40`, `:53`, `:72` | Pass the channel caller through the current command/helper signatures. Preserve resolution, formatting and 200-row paging. |
|
||
| CLI talk/follow with --thread | `cli.mjs:345`, `:349`, `:354`, `:382`, `:390` | Use the channel caller for this flow, including its receive/read operations. Non-thread talk/follow are unchanged. |
|
||
| WebUI chat | row-129 `chat.mjs:3`–`:12` | Wrap the supplied core call once with the webui channel caller; preserve all seven public method signatures. |
|
||
| Administrative CLI and other Console views | `cli.mjs:540`, `:544`–`:546`; row-129 webui `cli.mjs:59`–`:65` | Retain their existing raw core call. Never wrap the common transport factory globally. |
|
||
|
||
Both modules load only through `createLoader`; no core import names either
|
||
module. `kinds.mjs:9` is the registry; `loader.mjs:41`–`:69` already supplies
|
||
name/path/symlink/export/version checks. `hasModule` uses the same registry
|
||
(`loader.mjs:77`), so the existing organization setter can validate the new names.
|
||
|
||
## 2. Off behavior and the recovery path
|
||
|
||
The setting is, for example, `"plugins": { "channel": ["cli", "webui"] }`.
|
||
Only `cli` disables web chat; only `webui` disables the CLI's project/thread
|
||
surface; `[]` disables both. Removing the channel key also disables both. No
|
||
implicit default: an absent key means off (`loader.mjs:117`, plugins README:63).
|
||
|
||
A disabled CLI channel refuses `channel-disabled`, exit 3, before a project,
|
||
thread, delivery or read operation. A disabled WebUI channel returns HTTP 503,
|
||
`error: "channel-disabled"`, with no successful send receipt and no chat bus
|
||
operation. Both refusals include fixed, operator-facing recovery text:
|
||
"Run mosaic plugins to read the revision, then mosaic plugins set --file PATCH
|
||
with plugins.channel set to [\"cli\",\"webui\"]." Include the actual CLI
|
||
business selector when available. Assert the recovery text in both tests.
|
||
Do not edit proof transports.
|
||
|
||
The HTTP mapping actually belongs to row-129 `src/chat-http.mjs:12`–`:22`:
|
||
its closed STATUS table and safeMessage function decide the status and message;
|
||
its catch ignores arbitrary transport error text (`:114`–`:119`). `chat.mjs`
|
||
alone cannot deliver a new status or reflected message through that contract.
|
||
Propose adding only channel-disabled:503 and its fixed recovery message in
|
||
chat-http.mjs, with the seam adapting the refusal to BusReadError. This avoids
|
||
serve.mjs entirely and does not conflict with row 154's static routes. This is a
|
||
correction to the initial design's suggested serve.mjs mapping, accepted in
|
||
sage-0770. The fixed recovery text never echoes error input.
|
||
The chat page can still render its normal refusal state; other Console routes,
|
||
Settings, and core human administration stay available. An empty channel list
|
||
does not stop the bus or revoke the human session.
|
||
|
||
In particular, **`mosaic plugins` and `mosaic plugins set` do not use a channel**.
|
||
The current dispatcher already gives them the raw `callFor` (`cli.mjs:546`),
|
||
and `plugins.mjs:34`–`:37` calls `plugins` / `plugins.set` directly. The broker
|
||
requires a proved human and its configured organization adapter
|
||
(`broker.mjs:1133`–`:1165`), independent of any channel selection.
|
||
`plugin-settings.mjs:34`–`:60` applies a per-kind patch with revision checking
|
||
and the existing organization writer. None of that path changes.
|
||
|
||
Exact operator recovery, with both channels off:
|
||
|
||
1. From a real human shell outside an agent, run
|
||
`mosaic plugins --business BUSINESS --json` and obtain its `revision`.
|
||
2. Create an operator-owned patch file with
|
||
`{"revision":"<revision>","plugins":{"channel":["cli","webui"]}}`.
|
||
3. Run `mosaic plugins set --business BUSINESS --file PATCH`.
|
||
4. Retry chat. Its next settings read sees the restored selection; no unit
|
||
action or hand-edit of the organization file is needed for this channel helper.
|
||
|
||
Stale revisions require another read. An unavailable bus, invalid organization
|
||
file or uncertain organization transaction still fails closed through today's
|
||
writer; this design adds no repair bypass for those independent failures.
|
||
Being switched off cannot itself make the recovery read or write unavailable.
|
||
Legacy inbox/decide, non-thread talk, limits and usage remain core operations;
|
||
the channel switch here controls the project/thread surfaces named by the brief,
|
||
not the entire `mosaic` executable or human-proof transport.
|
||
|
||
### Cut-over and parity fixture exception (Sage and Mos accepted)
|
||
|
||
Existing organization files have no channel selection. Recommend an explicit
|
||
human-approved `plugins.set` patch enabling both modules in Mos's re-pin window,
|
||
after the new registry is running. Until that patch, only the converted chat
|
||
surfaces refuse; the independent recovery path remains available. Do not silently
|
||
enable omitted settings or change generic loader semantics to hide this cut-over.
|
||
Jason runs that plugins.set right after activate in Mos's re-pin window, then
|
||
smokes `mosaic threads --business BUSINESS` and Console chat. No agent runs this
|
||
step against the live organization file; see BUILD.md's cut-over notes. Sage accepted this cost in sage-0766 (about 16:45Z; stamp
|
||
corrected in sage-0768) and will put it in the re-pin plan.
|
||
|
||
There is also a concrete tension with the brief's unchanged-tests sentence.
|
||
CLI `thread-cli.test.mjs:35`–`:59` builds a broker without an organization adapter;
|
||
row-129 `chat.test.mjs:33`–`:43` records every injected bus call. A settings read
|
||
cannot pass those fixtures unchanged. Sage accepted this narrow exception in
|
||
sage-0766:
|
||
give the CLI fixture an organization read adapter returning enabled channels;
|
||
give the WebUI seam fixture the same `plugins` response, recording that management
|
||
read separately from chat operations. Keep every existing behavioral assertion,
|
||
HTTP fence, payload and expected result unchanged. No production test bypass or
|
||
default-enabled fixture mode in product code. Browser tests injecting chat methods
|
||
directly need no change; confirm this when row 129 lands. In those two files,
|
||
add only the organization read adapter or plugins response and a separate record
|
||
of management reads. Every existing assertion, payload and expected result must
|
||
remain byte-identical. BUILD.md will identify both fixture hunks for line-by-line
|
||
review. Mos accepted this narrow exception via sage-0769 / mos-0768.
|
||
|
||
## 3. Human proof remains entirely in core
|
||
|
||
Existing call graph stays:
|
||
|
||
```text
|
||
CLI core humanTransport ─┐
|
||
├─ core human-cli.mjs -> Client -> socket server
|
||
Console core humanCall ─┘ -> runtime.verifyHuman
|
||
-> Broker.bindHuman
|
||
channel dispatch receives only an existing call closure ─┘
|
||
```
|
||
|
||
No new direct caller of the shim, verifier or process reader. Modules never
|
||
import or spawn them. The new settings read uses the same existing call closure
|
||
and therefore the same proved route, not a second proof implementation.
|
||
|
||
| Proof input read | Exact source and unchanged use |
|
||
|---|---|
|
||
| `/proc/<pid>/stat` | `human.mjs:19`–`:21`: parent PID and start time for submitted process and traversed ancestors. |
|
||
| `/proc/<pid>/environ` | `human.mjs:25`–`:34`: only nonce and existing marker names retained; existing EACCES handling unchanged. |
|
||
| `/proc/<pid>/cmdline` | `human.mjs:41`: executable/argv ancestry checks and exact shim path. |
|
||
| `/proc/<pid>` metadata | `human.mjs:40`: lstat UID; not another file-content read. |
|
||
| Shim stdin, fd 0 | `human-cli.mjs:17`: strict request envelope; no token/config file added. |
|
||
|
||
The submitted process is read again before success (`human.mjs:89`–`:95`).
|
||
The proof also consumes the existing in-memory launch PID/start records and
|
||
resolved business human (`runtime.mjs:115`–`:121`); it opens no organization,
|
||
wrapper, shim-source, credential or system-config file to prove a request.
|
||
In particular, the verifier compares a **literal argv path**, not a file hash
|
||
(`human.mjs:70`). Hash checks below are regression evidence, not a new proof rule.
|
||
|
||
Unchanged core source and direct production callers:
|
||
|
||
| File | Current role/caller, unchanged |
|
||
|---|---|
|
||
| `packages/bus/src/human.mjs` | `verifyHuman` called by runtime only; exported `readProcess` also already used by human-cli, tester and tester-cli. No new caller. |
|
||
| `packages/bus/src/human-cli.mjs` | Self re-executes its own path (`:8`–`:12`); existing CLI transport and Console humanCall spawn it. No new caller. |
|
||
| `packages/cli/src/transport.mjs` | `HUMAN_CLI` resolved beside the running tree (`:12`), humanTransport at `:55`; existing CLI callers stay in place. |
|
||
| `packages/webui/src/bus.mjs` | Imports the same `HUMAN_CLI` (`:12`); existing humanCall spawn (`:27`–`:31`) and busCaller (`:60`). No new caller. |
|
||
| `packages/bus/src/runtime.mjs` | Same verifier import/call and same own-tree shim path (`:115`–`:116`). No edit. |
|
||
| `packages/bus/src/server.mjs` | Same authenticateHuman admission (`:62`–`:64`), capability envelope and refusals. No edit. |
|
||
|
||
Recorded unchanged byte baselines for later comparison:
|
||
|
||
```text
|
||
b995fc5cff3af8ade4c990d0e3bebf9d83829e00f034ebc9c3dcfd05f252b573 packages/bus/src/human.mjs
|
||
269c3a66008c0d57a54b14b73cf988d5f61f5e815455486296716e98ce5e8073 packages/bus/src/human-cli.mjs
|
||
5129c44b4d9f642e825344df65c9bfb4bac4fc8d6b25343b42d39853dbd40774 packages/cli/src/transport.mjs
|
||
dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24 packages/webui/src/bus.mjs
|
||
```
|
||
|
||
Bus, Console, wrapper and modules continue to run from the one pinned tree. No
|
||
copy/wrapper/unit change is proposed. The loader already resolves modules from
|
||
its own package tree (`loader.mjs:15`, `:41`–`:54`); pinned copies already include
|
||
the entire plugins package (plugins README:43–:46).
|
||
|
||
## 4. `via` is still the proof transport, not the channel selector
|
||
|
||
Keep `via: 'cli'` everywhere. WebUI already obtains that same human proof today.
|
||
`webui` names a selected presentation module, never a claim of human identity.
|
||
|
||
The brief's four call sites, at their current lines:
|
||
|
||
| Site | Value/read after this row |
|
||
|---|---|
|
||
| `runtime.mjs:121` (brief :117) | `bindHuman({ business, human, via: 'cli', outsideAgent: true })`, unchanged after verifyHuman. |
|
||
| `broker.mjs:530` (brief :526) | Protected role-end authorization still recognizes the human decision's `via === 'cli'`. |
|
||
| `broker.mjs:1887` (brief :1867) | Launch revoke/restore event still records `via: 'cli'`. |
|
||
| `landing.mjs:107` | Human resolution still requires `res.via === 'cli'` plus the existing actor/answer checks. |
|
||
|
||
Also leave broker `#human` (`:330`) and `bindHuman` (`:472`) unchanged. A module
|
||
does not set `via`, bind a session or supply the owner. No schema change or
|
||
existing decision/event rewrite is needed.
|
||
|
||
## 5. Discord is not connected
|
||
|
||
No Discord module, connector edit, binding/token read or new service. A future
|
||
`packages/plugins/channel/discord/index.mjs` can export `interfaceVersion = 1`
|
||
and `dispatch(request, { call })` with the same bus-operation/result contract.
|
||
That future row must provide an authenticated core caller bound to the right
|
||
human/business and map Discord interaction to the fixed operations; a module
|
||
cannot manufacture proof. This interface does not authorize that future ingress.
|
||
Its authentication, subscriptions and deployment are separate decisions; merely
|
||
adding a module of this kind does not require a kind-version bump.
|
||
|
||
## Proposed owned files, tests and mutants
|
||
|
||
Product: `packages/plugins/src/kinds.mjs`, proposed `src/loader.mjs` snapshot getter, new `src/channel.mjs`, new
|
||
`channel/cli/index.mjs` and `channel/webui/index.mjs`; CLI `src/cli.mjs`
|
||
and, only if passing the caller needs it, `src/thread.mjs`; WebUI `src/chat.mjs`
|
||
and proposed `src/chat-http.mjs` (only channel-disabled status/recovery text).
|
||
No serve.mjs edit planned. Docs:
|
||
`packages/plugins/README.md`, `docs/prd/mosaic-stack.md:822` and operator cut-over
|
||
notes in this packet. No public/ UI changes planned. New helper, snapshot getter
|
||
and chat-http mapping are explicitly proposed additions to the brief's call sites.
|
||
|
||
| Acceptance/guard | Named test file and killing assertion | Mutant |
|
||
|---|---|---|
|
||
| Both shipped modules load and forward through loader | new `packages/plugins/tests/channel.test.mjs`: each named module loaded, exact verb/args/result/refusal preserved | Remove registration, either export, or bypass dispatch; separate mutants. |
|
||
| Bad name, missing directory, symlink, missing export, wrong version | same new file, scratch loader tree following `tests/helpers.mjs:19` | One disabled guard per refusal, exercised specifically for channel. |
|
||
| Nonblocking matched snapshot | new plugins `channel.test.mjs`: names/modules stay paired across a switch; a blocked send does not prevent a concurrent list completing | Separate getters across an await; hold a selection lock across dispatch. |
|
||
| Only fixed operations | same file: unsupported verb refuses without calling transport | Remove operation allowlist. |
|
||
| CLI parity through actual loaded cli module | `packages/cli/tests/thread-cli.test.mjs`, fixture change described above; all current assertions retained; new `channel-cli.test.mjs` verifies module path | Bypass dispatch; change thread verb, arguments or result; each named killer retained in build table. |
|
||
| WebUI parity through actual loaded webui module | row-129 `packages/webui/tests/chat.test.mjs`, fixture change described above; retain `chat-browser.test.mjs` unchanged | Bypass module; change fixed REQUEST or mapped verb. |
|
||
| Independent off behavior | new `channel-cli.test.mjs` and `packages/webui/tests/channel-chat.test.mjs`: off CLI exit 3, off webui 503, both name recovery, zero chat/delivery/read calls; other channel still works | Ignore off selection; use wrong module; allow receive/read after off. |
|
||
| Both off and missing key recover | new `packages/cli/tests/channel-recovery.test.mjs`: real fixture organization writer + socket proof, plugins read/set work, next chat succeeds; stale revision refuses | Gate plugins management behind channel; cache selection forever; invent missing-key defaults. |
|
||
| Proof path, bytes, import/caller set unchanged | new `packages/bus/tests/channel-proof.test.mjs`; compare four baselines, source imports and actual fixture socket admission; direct no-proof request refuses | Substitute shim path; skip authenticateHuman; introduce module proof import/spawn. Mutants run only in detached fixtures. |
|
||
|
||
During implementation list each actual mutant and exact killing test in BUILD.md;
|
||
the table above is the design inventory, not a claim that any test has run.
|
||
Dewey reviews; Sage gates plugins, CLI, bus, WebUI and all shell suites. Request
|
||
the test slot before any run. Claim only after 129 is done and this design is
|
||
accepted by Sage and Mos. No live settings change or re-pin is part of the build.
|
||
|
||
## Design revision — sage-0766 / sage-0768
|
||
|
||
Sage accepted absent-key-off and the fixture-only exception. Mos subsequently
|
||
accepted the original design with those conditions (sage-0769 / mos-0768). Added operator recovery text, removed dispatch locking,
|
||
and proposed one matched snapshot getter because the loader has no such API.
|
||
HTTP inspection found that the existing chat-http.mjs table owns the mapping,
|
||
so propose that local edit instead of serve.mjs. Recheck every row-129 reference
|
||
against its landed tree after its test-only round 2. Row 164 remains unclaimed.
|
||
|
||
## Build start — sage-0794 / sage-0798
|
||
|
||
129 landed as 850484a7d; 164 claimed at rev 1793. Rechecked every cited row-129
|
||
production reference against that landed tree: chat.mjs:3-12, chat-http.mjs:12-22
|
||
and :114-119, WebUI cli.mjs:59-65; line numbers and behavior match. All four
|
||
proof byte baselines still match. See r1/BUILD.md for the six explicit existing
|
||
plugins expectation changes and the additions-only parity fixture hunks.
|
||
No chat.js edit, so optional F4 stays out. No product code or tests run yet.
|