Files
jason.woltje 068d868a99 lead: commit rocko's untracked evidence (4471 files: queue-124 queue-129 queue-171 queue-178 queue-280 queue-31 queue-84 queue-85 queue-94)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:15 -06:00

248 lines
15 KiB
Markdown

# Row 129 / #1623 — Console chat area
Design for Sage before product code, per sage-0719. Claimed at rev 1737.
Brief: docs/plans/2026-10-14_prd-1.2-build.md, The WebUI chat area,
pinned blob 333adea856b1. Requirements REQ-WEB-1, REQ-THREAD-2, REQ-USER-2.
Reviewer Filbert; landing gate Sage; public/ landing needs Mos's decision 123 go.
No tests, mutations, product edits or live actions have run for this design.
## Existing behavior and boundaries
Read landed row 121 (2c16d6b93), row 122 (7b5b682ae) and row 128 (ade628c8b):
- Row 121's broker supplies project.create/list and thread.create/list/open/
send/messages. A project is a bus catalog entry, optionally linked to a
configured repository key. It is not a new filesystem checkout. Each thread
belongs to its creating human and to the business's one threaded assistant
instance. A send/open asks the existing launcher to wake that session; its
persistent store and idle lifecycle already exist.
- Row 122 supplies the Console's human-only Settings write path and readout.
It demonstrates the separate payload boundary behind the same signed-in
send mode. Chat changes no model/limit setting or organization file.
- Row 128 supplies the human CLI's project creation, thread listing/creation/
opening/history, and talk/follow --thread. Its thread.send uses class REQUEST.
Chat uses those same broker verbs and ownership checks. Names are supplied
at creation: there is no existing project/thread rename verb, and this row
does not add one.
- Console routing is a static allowlist in serve.mjs, hash routing in
public/bus.js, and the legacy Board conversation in public/app.js. The
Board's /api/conversations, /api/conversation and /api/reply are separate
from managed business threads. Chat uses none of those legacy APIs.
- busCaller in src/bus.mjs uses the existing human CLI proof. The bus binds
the configured business human; the proxy's one --allow-user gates access
to that Console. Chat retains that existing single-user mapping. It does
not accept an owner/user/business/capability from a browser or invent a
multi-user identity bridge. The broker remains the ownership authority.
- Existing writes require --send, named preview Host, one exact authenticated
user header, exact HTTPS Origin, same-origin Sec-Fetch-Site, JSON and a
bounded payload. Existing Settings/project-file/message routes retain their
behavior. No GET is allowed to call a write, including thread.open.
## UI and hash routes
Add Chat to the existing section navigation and command palette:
| Hash route | View |
| --- | --- |
| #/chat | Projects and their owned named threads, plus New project |
| #/chat/projects/<project UUID> | One project, its owned threads, New thread |
| #/chat/threads/<thread UUID> | That owned thread's conversation and composer |
Project creation accepts a name and an optional configured repository key.
The optional selector reads the already-existing GET /api/business projects
key list; it never exposes or accepts a filesystem path. If that read is
unavailable, creating an unlinked project remains possible, as in the CLI.
Successful project creation opens its project page. Successful thread creation
opens its conversation page. No default project or thread is created implicitly.
The conversation shows its name, project, assistant instance, timestamped
messages and exact inert, pre-wrapped body text. User/assistant/chief messages
stay in the one thread selected. Names/bodies use the existing control/bidi
visibility convention and HTML-safe text; agent text never becomes executable
HTML. The view claims no running/working status that the thread API does not
report. A send receipt means accepted, not that the assistant has answered.
Reading a conversation does not write or wake anything. A separate explicit
Resume assistant button calls thread.open; sending a message also wakes through
the existing pending-message mechanism. Create, Resume and Send are unavailable
without signed-in send mode. All reads remain usable in the Console's existing
read-only mode, subject to the same human transport and owner check.
History uses the landed forward cursor, 100 messages per page (broker max 200),
chronological order. A visible Read next messages control pages forward; a full
page means more may exist, not a fabricated exact count. Once caught up, polling
reads after the last message id and de-duplicates by id. While an older page is
shown, the UI labels it and offers the next page instead of pretending it is
current. This keeps each request/render bounded and uses the existing API;
it does not add a latest-first/before cursor to the broker. The user can send
while reading history; the accepted-message receipt remains until that id is
seen in the conversation. This paging choice is part of the design for Sage.
Drafts/receipts/in-flight flags are scoped to thread id (and creation forms to
project), held only in memory. Polling preserves draft, focus, selection and
scroll; arriving messages scroll only when already at the tail or after the
user's own successful send. Otherwise show a new-message affordance. No chat
history/draft is persisted to localStorage. Pending submit disables that form;
there is one POST per explicit submit and no automatic retry.
The module uses request generations and AbortController on route changes.
A late A response cannot paint B or clear B's draft. Sign-out redirects/401
stop reads and clear private chat state. A thread ownership refusal clears its
history/draft and displays a refusal; it cannot reuse cached private content.
Ordinary read/network failures retain the current draft with a clear status.
Outcome-unknown writes retain no invented success/message; ask the user to
read the current list/history before submitting again. Sign-out still clears
private drafts, including after an uncertain write.
## HTTP routes and exact bus calls
New fixed /api/chat namespace; no arbitrary verb-forwarding endpoint. Browser
ids must be full UUIDs. No browser-selected tenant, owner or capability field.
Query names are allowlisted, single-valued and validated before any bus call.
| HTTP | Bus verb and arguments | Result |
| --- | --- | --- |
| GET /api/chat/projects | project.list {} | {rows,at} |
| POST /api/chat/projects | project.create {name,repo?} | verified {id,at} |
| GET /api/chat/threads[?project=UUID] | thread.list {project?} | {rows,at}; owned threads only |
| POST /api/chat/threads | thread.create {project,name} | verified {id,instance,at} |
| GET /api/chat/threads/UUID/messages[?after=UUID&limit=N] | thread.messages {thread,after?,limit}; N=1..200 | {rows,after,hasMore,at}; hasMore is conservative |
| POST /api/chat/threads/UUID/messages | thread.send {thread,body,class:'REQUEST'} | verified {id,at} |
| POST /api/chat/threads/UUID/open | thread.open {thread} | verified {opened,at} |
POST open accepts only an empty JSON object. Project/thread names obey the
broker's 1..200 character, no CR/LF/NUL rule. Body remains exact, with a nonempty
message and the existing bus character bound. Every POST also fits the Console's
16 KiB JSON byte bound; the browser checks encoded length and shows a normal
"message is too long" validation message rather than silently truncating.
No request can override the fixed REQUEST class. Names are not interpolated
into URLs; ids are encoded and validated. All replies are no-store JSON.
The new chat handler repeats the current Console send checks for every one
of its four POST operations: --send + named preview Host, exact single user
header, exact Origin, same-origin Fetch Metadata, JSON, size and exact shape.
One chat-local fence covers all four; it does not refactor the already-reviewed
Settings, file-editor or role-message fences. Every refused fence proves zero
bus calls. GET/open cannot accidentally write. Preview read-only and loopback
writes refuse, just as the existing Settings/file-editor writes do.
Transport comes from the same busCaller built by src/cli.mjs; injected fixture
transports test the handler. Adapter functions have fixed verb mappings and
forward only validated fields. Backend proof/ownership checks remain in place.
No change to packages/bus, packages/cli, schema, harness, sandbox or role grants.
### Refusals and uncertain writes
A same-business thread owned by someone else returns HTTP 403:
`{error:'thread-not-owned',message:'This thread is not available to this user.'}`.
The POST response adds sent:false. No name, owner, body, count or history is
returned. The bus's #thread check runs for messages/send/open, so a guessed id
cannot bypass ownership. A foreign-business/absent id is 404 thread-not-found.
Thread lists are filtered by the bus, never by hiding rows in the browser.
The existing project.list is a business-wide catalog (not a per-owner project
filter); this design preserves the CLI's catalog and displays only this human's
threads within it. It introduces no new project-ownership semantics.
Other mappings: invalid request/id/query/name/body/unknown repo → 400;
project-exists/thread-exists → 409; missing project/thread → 404;
human-required/unauthenticated/read-only → 403 (bad proxy user header → 401);
not-configured/no-bus-host/thread-instance-missing/ambiguous → 503;
unusable read response → 502. Fixed safe messages do not echo bus/parser data.
A write transport loss, malformed success id or uncertain response is 503
outcome-unknown, sent:'unknown'. A definite broker refusal is sent:false.
No catch/retry calls the writer again. Success ids/opened ids are checked before
acknowledging success. Log one line per write with action, signed-in user,
project/thread/message id if known and outcome, never names/body/capability.
## Files to create
- packages/webui/src/chat.mjs: fixed project/thread bus adapter.
- packages/webui/src/chat-http.mjs: strict reads, common chat POST fence,
validation, safe status/refusal/uncertainty mappings and response checks.
- packages/webui/src/public/chat.js: isolated chat UI, per-thread state,
fetch/generation/redirect handling, forms, history and polling integration.
- packages/webui/src/public/chat.css: scoped .chat-* layout/responsive styles.
- packages/webui/tests/chat.test.mjs: adapter/HTTP/broker ownership fixtures.
- packages/webui/tests/chat-browser.test.mjs: real browser behavior on fixtures.
- packages/webui/tests/chat-fixture.mjs if shared fixture setup merits a helper.
## Existing files to edit — shared ownership/landing list
- packages/webui/src/serve.mjs: import new route handler; add chat option;
add chat.js/chat.css to static allowlist; dispatch only /api/chat paths
before the generic preview write refusal. Preserve existing routes/branches.
- packages/webui/src/cli.mjs: construct the chat adapter from existing busCaller
and pass it to startServer. No new flag, proof, config source or live action.
- packages/webui/src/public/index.html: one stylesheet and one deferred script
before bus.js. This is a shared file with Researcher's row 154 asset includes.
- packages/webui/src/public/bus.js: Chat nav/command palette item; delegate the
three hash routes; refresh/pause/config-send hooks; auth forget hook; a small
chat-refresh event hook. Reuse existing render-generation handling, and
extend focused text-input selection preservation only if the chat forms
require it. No Docs/textarea/editor, Settings, Board or role composer rewrite.
- packages/webui/README.md and docs/TOOLS.md: chat routes, commands/parity,
write/privacy boundary and pagination. No package dependencies or lockfile.
public/app.js, public/markdown-preview.js, existing editor CSS, project reader/
writer and the legacy Board APIs are not edited. serve.mjs, index.html and
bus.js are the shared row-154 paths for Sage to order. Row 124 also edits
serve.mjs/bus.js/TOOLS and is in review: build on current landed refactor and
rebase/re-gate whenever one of those rows lands. Do not apply its unlanded patch.
A detached worktree will be created at the approved current base after this
response; this design stage touches only agents/rocko/work/queue-129 and logs.
## Acceptance evidence and planned mutants
No test runs without Sage's slot, including focused files. All eventual runs
use a scratch TMPDIR, never a launcher run under /tmp. No live Console/bus units.
1. Real broker + injected verified-human transport behind the HTTP adapter:
list projects and only owned threads, filter a project, create/names,
duplicate names, optional repo key, missing/ambiguous assistant instance,
direct deep link, cursor paging and invalid/cross-thread cursor. GET history
changes no delivery/read/open evidence. Existing thread owner/biz rules hold.
2. Seed another owner's thread with sentinel messages. Read, send and open each
return 403 with no sentinel, name or owner in body; list excludes it. A
foreign-business id is 404. Spy confirms no rejected write changed evidence.
3. For each POST: missing/duplicate/wrong user header; wrong/missing Origin;
wrong/missing Fetch Metadata; read-only/loopback; bad content type, oversize,
invalid/extra fields; bad id/name/empty body. Zero transport calls on refusal.
4. Success, definite refusal, lost reply and malformed success response. Exactly
one attempted bus write, correct sent status, no automatic retry. Fixed
thread.send class/identity and exact content are asserted at the adapter.
5. Browser: project/thread list and create, independent conversation routes,
send/receipt/following reply, Resume as explicit POST, page/de-dup order,
unread failure, read-only controls, draft/caret/scroll preservation, no POST
on polls/refreshes, late A response while B is selected, ownership refusal,
sign-out with pending fetch/write, hostile names/bodies/control characters.
6. One mutant per acceptance/security check, including each fence, ownership
status/no stale cache, fixed bus mapping, immutable send class, byte bound,
once-only submission, uncertain-success handling and thread-scoped state.
Proposed targeted gate: webui package suite (browser included), affected bus
thread tests and CLI thread parity tests, plus exact shell readers of the
changed files if any. Sage chooses/grants the slot and runs the landing gate.
## Scope ruling — sage-0724 (approved)
The old Q156 conversion-order paragraph in the pinned file says row 128 makes
cli a channel module and row 129 adds webui. The revised, landed row 128 brief
(docs/plans/2026-10-18_row128-thread-cli.md) owns CLI only, and no channel kind
exists in packages/plugins/src/kinds.mjs; its current kinds are usage-source,
limit-check and search-source. Implementing that older paragraph would add a
new core interface and settings contract beyond this UI design.
Sage approved this design in sage-0724. Row 129 stays UI-only on the current
human transport. No channel kind, CLI module or WebUI channel module is built
here; that core interface gets a separate brief after 129. chat.mjs is the
single HTTP-to-bus seam for that conversion. Forward paging, creation-only
names and the 403/404 ownership distinction are approved as documented above.
## Build start
Detached worktree: /mnt/storage/src/mosaic-stack-rocko-129.
Base: 5e0437659 (contains landed 640f4cef5; later two commits are own logs).
No unlanded row 124 or 154 source is applied. Ask Sage for the slot before the
first focused run; no suite, browser or mutant run until granted.