Files

20 KiB
Raw Permalink Blame History

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:

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:

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:

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:

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.