Files

304 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.