A copy binding grants the repositories its projects' remotes name when a credential is issued, and its decision records only project names. repo set (CLI, route, Settings tab) is now refused with project-repo-bound while a pending or bound copy binding names the project, before any check, origin change or write, and again under the lock. The message names the copies and the next step: revoke the binding, set the repository, raise a new binding decision. An unbound binding, or none, behaves as before. Bindings are read from the loader-validated document; binding, launcher and broker code are unchanged. New refusal-table entry for project-repo-bound.
1386 lines
90 KiB
Markdown
1386 lines
90 KiB
Markdown
# Mosaic Console
|
||
|
||
Piece 4, #1507. The control board is the first and only WebUI screen.
|
||
|
||
## Console actions
|
||
|
||
This is the write inventory for the organizations release. A CLI name in the
|
||
third column identifies an existing or planned operation; it does not by itself
|
||
establish equivalent authorization, revision checks, or receipts. Unless
|
||
marked enabled, Console mutations remain unavailable until that parity is
|
||
verified. The `--org` forms below are release contracts, not claims about the
|
||
baseline CLI.
|
||
|
||
| Console write | CLI operation | Organization-release status |
|
||
| --- | --- | --- |
|
||
| Create organization | `mosaic org create <id> --from <file>` | S6 integration hook |
|
||
| Save organization or theme | `mosaic org update <id> --from <file> --if-revision <revision>` | S6 integration hook |
|
||
| Initialize organization checkout | `mosaic org queue init <id> --local-only --accept-local-loss-risk` or `--remote <url>` | S6 integration hook |
|
||
| Initialize project Git | `mosaic org project repo init <id> <project> --local-only --accept-local-loss-risk` or `--remote <url>` | S6 integration hook; remote mode sets and checks `origin` (#1873 P1-6) |
|
||
| Change a project's repository | `mosaic org project repo set <id> <project> (--local-only --accept-local-loss-risk \| --remote <url>) --if-revision <revision>` | Enabled (#1873 P1-7): the owner's Repository section on an organization project's Settings tab, `/api/orgs/<id>/repo/project/<project>`; needs send mode; refused (409 `project-repo-bound`) while an approved or active copy binding names the project |
|
||
| Recover a stopped bus with stale host state | `mosaic bus recover <id>` | CLI only |
|
||
| Inspect or clear a dead lifecycle lock | `mosaic org lock show <id>` / `mosaic org lock clear <id>` | CLI only |
|
||
| Start, stop or restart a bus | `mosaic bus start <id>` / `mosaic bus stop <id>` / `mosaic bus restart <id>` | S6 integration hook |
|
||
| Restart (or first start) an organization's bus | `mosaic bus restart <id>`, or `mosaic bus start <id>` for a bus never started | Enabled (F3): the owner's **Restart bus** control, offered where a page says the bus is down or a change needs a restart; trailed |
|
||
| Restart the Console | Stop and run `mosaic console start` again | Enabled (F3) when `mosaic console start` runs it in the foreground, or with `--dev-local-owner --detach` under a detached supervisor (D1): Settings > Console; the owner only; trailed. Other Consoles answer `not-supervised` |
|
||
| Save, review, confirm or reverse an organization's autonomy | Read, save and review: `mosaic org autonomy ORG`, `mosaic org autonomy ORG set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours 1-168] [--authority-resolver NAME|""]`, `mosaic org autonomy ORG review CLASS` (E2, through this route and its revision check); confirm and reverse act on inbox decisions and have no verb yet | Enabled (D1) on a `--dev-local-owner` Console: the owner only, on the selected organization's bus, behind a fence that passes only the four console verbs `autonomy.set.console`, `autonomy.review.console`, `decision.confirm.console` and `decision.reverse.console` |
|
||
| Hire, fire or change an agent; read role templates | `mosaic org role-templates ORG`; `mosaic org hire|change ORG --name NAME --template TEMPLATE --harness H --model M [--thinking T] [--level org|project|root] [--project P] --reason TEXT`; `mosaic org fire ORG --name NAME [--level ...] --reason TEXT` (E2, through this route) | Enabled (D4) for the owner on a selected organization's bus: one decision raised and approved through `hire.start.console`, with the bus's hire checks |
|
||
| Create, edit or archive a board task | `mosaic tasks create --title T --requirement REQ [--project P] [--state S] [--assignees R,...] [--priority 0-5] [--due ISO|none]`; `mosaic tasks edit task:N --expect DIGEST ...`; `mosaic tasks archive task:N --expect DIGEST` (E2: the board's own check, then the person's `tasks.create|edit|archive` on the bus) | Enabled for the owner on an organization with the built-in task store |
|
||
| Create chat project linked to a configured repository | `mosaic project create <name> --repo <project> --org <id>` | Enabled |
|
||
| Create a thread | `mosaic thread create <project> <name> --org <id>` | Planned enabled |
|
||
| Open a thread | `mosaic thread open <thread> --org <id>` | Planned enabled |
|
||
| Send a thread message | `mosaic talk --thread <thread> <text> --org <id>` | Planned enabled |
|
||
| Start the onboarding interview (F2) | `mosaic thread create <chat-project> "Onboarding interview" --instance <assistant> --org <id>`, then `mosaic talk --thread <thread> "$(mosaic project interview <project>)" --org <id>` | Enabled through the thread create and send routes above |
|
||
| Approve the north-star draft (F2) | `mosaic project north-star <project> --org <id> --approve <draft-commit> --base <spec-commit>` | Enabled: copies the reviewed draft into `spec/north-star.md` through the human save (`project.file.save`) |
|
||
| Archive or unarchive a chat project | `mosaic project archive` / `mosaic project unarchive` | Unavailable pending parity |
|
||
| Open a room or send a room message | No CLI equivalent identified | Unavailable |
|
||
| Share or unshare a thread | No CLI equivalent identified | Unavailable |
|
||
| Create, clear or cancel a thread session | `mosaic session create`, `mosaic session clear`, `mosaic session cancel` | Unavailable pending parity |
|
||
| Send a role message | `mosaic talk <role> <text>` (different message class) | Unavailable pending parity |
|
||
| Mark a board session seen/unseen; reply to a board seat | No CLI equivalent identified | Unavailable |
|
||
| Approve or deny a decision | `mosaic decide <id> <choice> --org <id>` | Owner-only confirmation and resolution on the selected organization bus |
|
||
| Answer, attach, change mode, cancel or revoke a request | No equivalent CLI identified | Unavailable |
|
||
| Edit or reset launch/assistant limits | `mosaic limits set` / `mosaic limits reset` | Enabled (F3) for the owner on a selected organization's bus; unavailable on other Consoles pending parity |
|
||
| Rename an instance | `mosaic name` | Enabled (F3) for the owner on a selected organization's bus; unavailable elsewhere pending parity |
|
||
| Stop an assistant | No equivalent CLI identified | Enabled (F3) for the owner on a selected organization's bus; unavailable elsewhere |
|
||
| Add, change or delete an instance; launch, regenerate or terminate an agent; change a veto | `mosaic org instance add`, `set`, `delete`, `launch`, `regenerate`, `terminate`; `mosaic org veto` (old spelling `mosaic business instance`, `mosaic business veto`) | Enabled (F3) for the owner on a selected organization's bus, with **Restart NAME now** (terminate, then launch) after a route change; unavailable elsewhere pending parity |
|
||
| Change routes or enabled harnesses/providers | `mosaic org instance set`; `mosaic plugins set` | Enabled (F3) for the owner on a selected organization's bus; the page lists how the change reaches each saved agent; unavailable elsewhere pending parity |
|
||
| Halt or resume processing | `mosaic halt` / `mosaic resume` | Unavailable pending parity |
|
||
| Save a project file or revert library content | No equivalent CLI identified | Unavailable |
|
||
| Change a project remote | No equivalent CLI identified | Unavailable |
|
||
| Start, submit or cancel proxy sign-in; add an API key; set account priority or models | No equivalent CLI identified | Enabled (F3) for the owner on a selected organization's bus once its proxy is set up; setup itself stays `mosaic proxy init` in a terminal |
|
||
|
||
Browser navigation, local appearance preferences and unsaved drafts are local
|
||
state, not server writes. Signing out is a session action.
|
||
|
||
F3 writes (Console restart, bus restart, and the owner's routing, agent and
|
||
instance changes on an organization) append one line each to
|
||
`<dataRoot>/console/actions.jsonl` (0600): who (provider, issuer, subject,
|
||
name), when, what and the outcome. A restart that cannot write its line is
|
||
refused. On a Console with sign-in, no tester, granted user or API token
|
||
reaches these controls: each needs the signed-in owner, the page's exact Origin
|
||
and `Sec-Fetch-Site: same-origin`. Those headers stop another site's page, not
|
||
a local program, which can send any header it likes.
|
||
|
||
A dev local owner Console (`--dev-local-owner`) has no sign-in. Every request
|
||
that passes the Host and Origin checks acts as the owner, and this includes the
|
||
F3 controls. An agent launched on the same machine runs as the same OS user, so
|
||
it can restart the Console or a bus and change routing, agents and settings just
|
||
as the owner's page can. The startup warning says so. Use a dev instance for UI
|
||
testing only. The real boundary is sign-in (`--sso`); a refusal for local agent
|
||
peers in dev mode is a recorded follow-up.
|
||
|
||
### Route registration
|
||
|
||
A module may export `registerRoutes(table)` and be included with one entry in
|
||
`startServer({ routes: [registerRoutes] })`. It calls
|
||
`table.add('GET' | 'POST', absolutePathOrAnchoredRegExp, handler)`. A handler
|
||
receives `(req, res, { path, search, session, orgId, origin })`. The server
|
||
checks sign-in and, for writes, exact Origin and same-origin fetch metadata;
|
||
organization-scoped hooks also resolve the organization before dispatch.
|
||
Handlers must validate their own input, enforce their own service authorization
|
||
and return bounded responses. Scoped handlers receive only an organization ID,
|
||
never a client-supplied socket or path.
|
||
|
||
## Run
|
||
|
||
Start the existing board in one terminal, then the WebUI in another:
|
||
|
||
```sh
|
||
node packages/control-board/src/cli.mjs serve
|
||
node packages/webui/src/cli.mjs serve
|
||
```
|
||
|
||
Open http://127.0.0.1:7330/. Optional `--port N` and
|
||
`--board http://127.0.0.1:7331` select another port or loopback board origin.
|
||
`--business ID` picks the business the bus views read; without it they read
|
||
the running bus host's business (`mosaic bus start <business>`).
|
||
Port 0 picks a free port. Ctrl-C stops each foreground server. No daemon,
|
||
installation, account, login or deployment is added. The preview mode below
|
||
checks one header a sign-in proxy sets; it signs nobody in itself.
|
||
|
||
Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback
|
||
by default, rejects a repeated Host (400), nonlocal Host and cross-origin
|
||
requests, sends no CORS
|
||
headers, refuses redirects and accepts only JSON object POST bodies up to 4096
|
||
bytes (16 KiB for row 76's send, below). Do not put the board behind a public proxy. Put the Console behind one
|
||
only in the preview mode below, with a proxy that meets its contract.
|
||
|
||
## Preview behind a sign-in proxy (row 74, #1568)
|
||
|
||
```sh
|
||
node packages/webui/src/cli.mjs serve --port 7332 \
|
||
--public-host NAME --auth-header HEADER --allow-user ID
|
||
```
|
||
|
||
The three flags go together; one or two refuse at start, before any config
|
||
is read. NAME is a DNS name, no scheme or port. HEADER is a header name other
|
||
than Host or Origin; Node lowercases incoming header names, so it matches
|
||
without regard to case (`X-Example-Uid` on the flag finds `x-example-uid`).
|
||
ID is the one user id, 1 to 256 visible ASCII characters without a comma,
|
||
and the value compare is exact. The Console still binds to loopback only. It
|
||
names no identity provider and has no login code: the proxy signs the user
|
||
in and limits sign-in to the right group.
|
||
|
||
In preview mode:
|
||
|
||
- A request whose Host is exactly NAME must carry HEADER once, with a value
|
||
equal to ID. Missing, different or repeated answers 401 `sign-in required`
|
||
before any route runs, so no bus read or board call is made. An Origin,
|
||
when sent, must be `https://NAME`.
|
||
- A request whose Host is loopback on the Console's port (`127.x`,
|
||
`localhost` or `[::1]`) is checked as before and needs no HEADER, so the
|
||
operator's browser on the host still works. Any other Host is 403.
|
||
- A request that carries HEADER with any Host but NAME answers 403 `proxy
|
||
Host refused`. The operator's browser never sends HEADER; a proxy that
|
||
rewrote Host does, and it fails on its first signed-in request instead of reaching
|
||
the loopback path unchecked.
|
||
- More than one Host header answers 400 before any other check, in this
|
||
mode or not. Node keeps the first, so a loopback Host followed by
|
||
`Host: NAME` would otherwise skip the sign-in check.
|
||
- Every method other than GET and HEAD answers 403 `read-only`, whatever the
|
||
Host. `/api/seen` and `/api/reply` never reach the board, so Seen and Reply
|
||
fail in the page with that refusal. The one exception is POST
|
||
`/api/bus/message` when the Console was started with `--send` (next
|
||
section).
|
||
- The GET routes are unchanged: the bus views, the Queue, Business and
|
||
Settings reads, the board panel and the assets. If the board is down, its
|
||
panel shows the existing error and the bus views still answer.
|
||
|
||
What the proxy must do. The Console trusts HEADER only because the proxy
|
||
controls it:
|
||
|
||
- It terminates TLS for NAME and is the only thing that reaches the port
|
||
from off the host.
|
||
- It strips HEADER from every client request and sets it only after the
|
||
identity provider signs the user in.
|
||
- It forwards only requests whose Host is NAME, and forwards that Host
|
||
unchanged. nginx rewrites Host to the upstream by default; set
|
||
`proxy_set_header Host $host;` (or the literal NAME). Caddy and Traefik
|
||
keep the client's Host by default.
|
||
- Any loopback Host on the Console's port, `localhost` included, is the
|
||
operator's path and skips the header check, so a proxy that forwarded a
|
||
client's `Host: 127.0.0.1:7332` would let anyone through without signing
|
||
in. A tunnel whose local end uses the same port number sends such a Host
|
||
and reaches the Console unchecked. Anything that reaches a tunnel's local
|
||
end can send a loopback Host without HEADER, so bind that end to loopback.
|
||
- Every hop on this host that reaches the loopback port is bound by this
|
||
contract too, not only the edge.
|
||
- It passes the Console's status and body through and doesn't rewrite
|
||
upstream errors (nginx's `proxy_intercept_errors` with an `error_page`,
|
||
for example). Under `--send` the page reads a 4xx other than 408 as "not
|
||
sent", so a proxy that turned the Console's 503 `outcome-unknown` into a
|
||
4xx or a redirect would report a message that may exist as not sent.
|
||
|
||
The local exposure doesn't change ("Data and boundaries"): any process on
|
||
the host can still connect to the port and send `Host: NAME` with HEADER and
|
||
ID, or just a loopback Host, and read what a reader capability reads. The
|
||
user id is not a secret; the sign-in at the edge is what keeps others out.
|
||
Header preview is read-only. A configured proxy proof does not permit writes;
|
||
`--send` requires the Console's verified OpenID Connect `--sso` mode.
|
||
|
||
To run the Console as a systemd user unit, `scripts/webui-service.sh`
|
||
renders and installs one with these flags (row 75; `docs/TOOLS.md`
|
||
"Console service"). Jason starts and restarts it, and so may Mos under lead
|
||
decision 102, which accepts that sends from a Console Mos started enter the
|
||
trail as Jason. Any other agent starting it works around the human proof
|
||
(lead decisions 62 and 94). Without `--send` the unit is read-only. The script takes `--send` (row 83)
|
||
only with `--sso`; otherwise it exits 1 and writes nothing.
|
||
With it, the unit's ExecStart ends with `--send`, and the Console needs the
|
||
system config at start: without it serve refuses with `--send needs the bus
|
||
configured` and exits 2, which the unit doesn't restart. `status` says
|
||
whether the installed unit sends.
|
||
|
||
The unit can run a read-only shared stack copy (rows 77 and 85), with
|
||
serve's --checkout naming the canonical checkout so the queue stays current.
|
||
webui-service.sh copy calls stack-copy.sh copy: the default directory is
|
||
mosaic-stack/copies/<full id>, and commits must be on both local and pushed
|
||
refactor. The copy includes bus/CLI/Console code, adapters and pinned npm
|
||
dependencies. install --copy ID refuses unless the bus's effective
|
||
ExecStart and WorkingDirectory name that same copy, including drop-ins.
|
||
It also refuses while the Console runs. With --auth-header X-authentik-uid,
|
||
allow-user must be the 64-character Authentik uid (decision 101). Status
|
||
names the copy and refactor drift. Files and directories are read-only;
|
||
copies are retained. Existing Console-only copies are not migrated or pruned.
|
||
|
||
With `--instance NAME` (row 190) every verb works on
|
||
`mosaic-webui-NAME.service`, which sets NAME's `MOSAIC_CONFIG`; install
|
||
checks NAME's `mosaic-bus-NAME@` units for the copy. render and install then
|
||
need `--port`, and 7330, 7332 or 7333 as the main or legacy tester port exits 4
|
||
with port-reserved. For every instance, install refuses a `--port` or
|
||
legacy tester port that another installed Console unit in the unit directory
|
||
already takes. That refusal is port-claimed, exit 1, and writes nothing. A
|
||
unit with no `--port` holds 7332. See
|
||
[TOOLS.md, Stack instances](../../docs/TOOLS.md#stack-instances---instance-name-row-190).
|
||
|
||
The single update and rollback sequence is
|
||
[TOOLS.md, Pinned stack update](../../docs/TOOLS.md#pinned-stack-update).
|
||
A bus restart ends managed sessions, so no PM turn may be in flight.
|
||
Switching the Console, bus or wrapper, changing any unit, and starting or
|
||
stopping either service requires Mos's go for that live step. Agents build
|
||
and test fixtures; they stop before the live sequence.
|
||
|
||
## Sign-in in the Console (Q175 phase 1, #1690)
|
||
|
||
`serve --sso [--send] [--tester-socket]` makes the Console an OpenID Connect
|
||
relying party. It reads `console.sso` from the system config (`publicOrigin`
|
||
and one or more providers, each with its issuer, client id, a client secret
|
||
slot, the signing algorithms it accepts and the subjects allowed in). The
|
||
design is `agents/sage/work/q175-sso/DESIGN.md`. `--sso` can't be combined
|
||
with `--public-host`, `--auth-header`, `--allow-user` or `--proof-*`; `--send`
|
||
requires `--sso`. Header-preview mode cannot enable the human writer.
|
||
|
||
Phase M3 sign-in requires an active account for account authority. Run
|
||
`mosaic accounts init` before the first accounts sign-in; configured SSO subjects
|
||
without accounts are refused. Legacy access.json users can still reach the root
|
||
business under their live grants. A session records account, legacy user and root
|
||
ownership separately. Only an active operator whose subject is configured as a
|
||
root owner gets the root human transport; an account alone gets 404 on root data
|
||
routes. Restarting the Console requires the instance operator role.
|
||
|
||
Authentication generations are checked on every request: disable/re-enable,
|
||
role or subject changes and `user signout` invalidate earlier account sessions.
|
||
Legacy root grants remain independent. There are at most 512 sessions and 128
|
||
pending logins, with eight sessions per account; a ninth evicts that account's
|
||
oldest. Sign-out removes the current session entirely.
|
||
|
||
Every stack copy carries a read-only `.stack-build.json` naming its commit.
|
||
A stamped Console requires `--sso`, a valid mode-0444 `.deployed` naming its
|
||
instance, and a loadable accounts file. A malformed stamp refuses every start.
|
||
Stamped Console and bus starts refuse dev flags. Checkout development still
|
||
supports the named local dev owner, but refuses it if accounts.json, console.sso
|
||
or `.deployed` is present. Accounts instances use `case2-unit.sh` system units;
|
||
`webui-service.sh` user units remain a checkout development path.
|
||
|
||
- The client secret is read once at start from
|
||
`<config dir>/secrets/mosaic-stack/<slot>`. The start refuses a slot that is
|
||
a symbolic link, not a regular file, owned by another user, has any group or
|
||
other mode bit, or sits in the repository, the `--checkout` directory or a
|
||
stack copy. The value is never printed.
|
||
- At start the Console fetches each provider's discovery document and JWKS. A
|
||
provider that can't be reached (a network error, a timeout, a status other
|
||
than 200, or a body that isn't JSON) leaves the Console up with that
|
||
provider's `/auth/login` at 503 and a retry every 5 s, doubling, capped at 5
|
||
minutes. A document that fails a check (issuer, algorithms, S256, a usable
|
||
key) refuses the start. `/healthz` adds `sso: { <id>: "ready" | "unreachable"
|
||
| "refused" }`.
|
||
- The Console listener serves only the Host of `publicOrigin`, whatever port
|
||
it listens on. Any other Host, a loopback one included, gets 403 `public
|
||
Host required`, except `GET /healthz`, which stays open on loopback for the
|
||
status and release gates. The loopback operator path in the preview section
|
||
above does not exist under `--sso`.
|
||
- Without a session, `/api/*` gets 401 `sign-in required` and a page load gets
|
||
a 303 to `/auth/login?return=<path>`. The page then shows the signed-out line
|
||
with a link back to sign-in. A session lasts at most 8 hours and ends after 1
|
||
hour without a page load or a write; a restart ends every session.
|
||
- The page shows Sign out only under `--sso`. It posts to `/auth/logout`, which
|
||
ends the session and goes to the provider's end-session endpoint when it has
|
||
one, or to `/auth/signed-out`.
|
||
- With `--tester-socket`, the tester listener refuses any request carrying an
|
||
`Authorization` header, a `__Host-mosaic_session` or `__Host-mosaic_login`
|
||
cookie, or an `x-authentik-*` header (403 `tester listener takes no
|
||
sign-in`). It never serves `/auth/*`.
|
||
|
||
## Message a role (row 76, #1570)
|
||
|
||
```sh
|
||
node packages/webui/src/cli.mjs serve --port 7332 --sso --send
|
||
```
|
||
|
||
`--send` is off by default. It requires verified OIDC through `--sso` and
|
||
refuses header-preview configurations, including those with proxy proof.
|
||
It also needs the system config, including `console.sso`, where a read-only
|
||
Console would still serve the board. A unit that sends comes from
|
||
`scripts/webui-service.sh install` with `--send` (above). With `--send` the
|
||
Inbox has a "Message a role" box: a role picker filled from the agents read,
|
||
a text box and Send. The message reaches the role as a message from the
|
||
human, class INFO,
|
||
through `message.send` on the human transport. It goes the same way as one
|
||
sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
|
||
decision 95). Decisions are still answered in a
|
||
terminal. There are no other message classes, no corrections and no seat
|
||
conversations here.
|
||
|
||
With `--send`, the Console authenticates each user through its OIDC session
|
||
and uses that user's authorized transport. Caller-supplied identity headers
|
||
and a loopback connection do not authenticate a writer.
|
||
|
||
The request is POST `/api/bus/message` with a JSON object `{to, body,
|
||
in_reply_to?}`. Nothing is sent unless all of these hold, each checked on
|
||
every request:
|
||
|
||
- Host is the configured OIDC public Host and the request has a valid
|
||
signed-in session. There is no loopback operator path under `--sso`.
|
||
- Origin is exactly `https://NAME`, else 403 `cross-origin request refused`.
|
||
- `Sec-Fetch-Site` is sent once and is exactly `same-origin`, else 403
|
||
`cross-site request refused`. Browsers set it, and page script can't.
|
||
- Content-Type is `application/json` and the body is at most 16384 bytes
|
||
and a JSON object with `to` and `body`, optionally `in_reply_to`, and no
|
||
other key. Anything else is 400 `invalid-request`.
|
||
|
||
The Console doesn't judge `to` or the body beyond that; the broker does,
|
||
with `message.send`'s own rules. Its body limit, 32768 characters, is past
|
||
the request limit, so the 16 KiB limit binds first. A broker refusal means
|
||
nothing was written. It answers `{ error, message, sent: false }` with the
|
||
read routes' statuses: 400 for `unknown-role`, `invalid-request`,
|
||
`message-not-found` and `request-too-large`, 403 for `human-required` and
|
||
`read-only`, 503 for `not-configured` and `no-bus-host`, 502 otherwise.
|
||
|
||
When the transport can't say what happened (a timeout, a child that exits
|
||
without a code, an answer that can't be read or that has no message id), the
|
||
message may have been written. The answer is 503 `outcome-unknown` with
|
||
`sent: "unknown"`. Nothing retries, in the server or in the page.
|
||
|
||
Each send that gets past the sign-in check writes one line to the Console's
|
||
stdout: the time, `send`, the user, the `to` value (JSON-quoted, at most 64
|
||
characters), then `id=<message id>` or `error=<code>`. The body is never
|
||
logged. A request refused at sign-in or by the shared Host and Origin
|
||
checks writes no line.
|
||
|
||
On the page:
|
||
|
||
- The draft, role and text, survives the ten-second refresh, kept in page
|
||
memory like a Reply draft. Reloading the page loses it. Send is disabled
|
||
while a send is pending.
|
||
- Sent shows the message id and links its trail, `#/trail/message/<id>`:
|
||
the message and its deliveries. A reply is not in the trail; it shows
|
||
under the receipt (below). The text is cleared unless it was edited
|
||
during the send.
|
||
- Not sent shows the broker's code and keeps the draft. A redirect counts
|
||
as not sent: the Console never redirects this route, so only the sign-in
|
||
proxy does, when its session has expired. The page asks for a reload to
|
||
sign in again, and signs out (row 95, below).
|
||
- Outcome unknown says the message may have been sent, keeps the draft, and
|
||
has no trail to link, since no id came back. Check the role's side, or
|
||
the bus trail from a terminal, before sending again. A 408 counts as
|
||
unknown, here and on the board's Reply, since a proxy can answer it
|
||
after forwarding the request (Envoy's `stream_idle_timeout`).
|
||
|
||
The role's reply comes back as a message to the human with `in_reply_to`
|
||
set to the sent message's id. Since row 81 (#1575) the Sent receipt lists
|
||
those replies, oldest first, each with its role, time, class, body and a
|
||
trail link. It reads them with the bus `replies` read on every render, so a
|
||
reply shows on the next ten-second refresh or Read again. "No reply yet"
|
||
means the read returned none; "could not be read" means it failed, and the
|
||
next refresh tries again. The bus returns at most 50; at 50 the receipt says
|
||
only the first 50 show. The list belongs to the last Sent receipt: a new
|
||
send starts its own, and reloading the page drops it.
|
||
|
||
The Console only shows replies. It never receives a message or marks one
|
||
delivered or read: `replies` writes no `deliveries` row. So Jason's next
|
||
`mosaic talk` in a terminal still prints each reply, within talk's limits:
|
||
|
||
- talk receives only after it sends its own message;
|
||
- `--wait 0` returns straight after the send and receives nothing;
|
||
- otherwise each receive returns every undelivered message to the human, up
|
||
to 100 a call. The broker records each one delivered when
|
||
`message.receive` returns it, so the next receive doesn't return it
|
||
again; talk prints each one and marks it read.
|
||
|
||
## Use
|
||
|
||
- Waiting on you uses the board's `waitingOnYou` flag across all projects.
|
||
- Select a project on the left to filter the session table. Counts show visible
|
||
rows out of the total when Hide offline or Hide seen hides anything.
|
||
- Select an agent to open its inspector. Arrow keys move between table agent
|
||
buttons, Enter opens, and Close or Escape in the inspector returns focus.
|
||
- Seen clears the row from Waiting on you until the board detects new activity.
|
||
The collapsed Seen section keeps those rows available; Unsee returns them.
|
||
- Reply appears only where the board's registration permits it. A failure shows
|
||
the board's stderr and keeps the draft. Transport success is the board's
|
||
`delivered` receipt, not proof that the seat processed the message.
|
||
- Refresh runs the existing board scan. Automatic refresh uses the board page's
|
||
ten-second interval; Pause stops it. Failed refreshes keep the last snapshot
|
||
with a warning. There are no automatic action retries.
|
||
- Palette and appearance are the installed `theme` plugins (see "Themes" below).
|
||
Harbor in the dark is built in and is the default. This browser keeps its own
|
||
choice per organization when local storage is available; with none, the
|
||
organization's saved `appearance` applies. Settings (row 54) shows the same
|
||
control and nothing else that changes.
|
||
- History opens a read-only conversation view for a seat, from its Waiting
|
||
card, its table row or its inspector (#1507, CHAT-02). It shows the whole
|
||
branch with nothing clipped. Tool calls, tool results and thinking start
|
||
collapsed. Session text is always shown as text: Markdown stays as source,
|
||
and terminal controls, bidi controls and marks show as visible symbols. Reply in the view
|
||
uses the same board reply path as the inspector.
|
||
- The view checks for new entries on the same ten-second refresh, and Pause
|
||
stops it. It never switches on its own. A fork, a newer session for the seat
|
||
or a rewritten file each shows a marker with a button to open the other
|
||
history. Reloading after a rewrite keeps the branch the view was on; if that
|
||
branch is gone, the view opens the latest one and says so. Session lists
|
||
older sessions for the seat, and is empty while the list is read or when
|
||
that read fails. Only repository Pi seats
|
||
have history; other harnesses say so.
|
||
|
||
Drafts and receipts stay in page memory, including across refresh, inspector
|
||
changes and a stale registration. Reloading or closing the page loses them.
|
||
A pending send stays disabled across refresh; new text typed during a send is
|
||
not cleared by the earlier send's success. If the proxy loses the response,
|
||
delivery may be unknown: inspect the seat before sending again.
|
||
|
||
## Inbox, tasks, agents and trails (slice 1 S5, #1522)
|
||
|
||
The Console sidebar adds Inbox, Tasks and Agents next to the control board,
|
||
which stays at `#/`. Routes: `#/inbox`, `#/inbox/<decision>`, `#/messages`, `#/tasks`,
|
||
`#/tasks/<vikunja:project/task>`, `#/agents`, `#/trail/task/<ref>`,
|
||
`#/trail/decision/<id>` and `#/trail/message/<id>` (row 76), with `?kind=` filters on a trail. The design
|
||
note is `agents/dewey/work/wui/SLICE1-VIEWS.md`.
|
||
|
||
- Every view reads the four verbs of the Q1 module (`packages/bus/src/views.mjs`,
|
||
lead decision 56): `inbox`, `tasks`, `agents` and `trail`. The CLI reads the
|
||
same functions, so both show the same broker data.
|
||
- Nothing writes but the "Message a role" box, the Settings update, the Assistant
|
||
section's three buttons and the spec and docs editor's Save, all under `--send` (above,
|
||
"Assistant settings" and "Spec and docs editor"), and the decision answers below.
|
||
A decision shows `mosaic decide <id> <key>` for each choice,
|
||
with Copy, which only puts the command on the clipboard. This optional terminal
|
||
fallback remains available (REQ-DEC-3, Q4). Seen is not set from the browser. The
|
||
command has no `--business`, so `mosaic decide` uses the running bus host's
|
||
business, as the Console does by default. From a Console started with
|
||
`--business` for another business the copied command refuses (no bus host,
|
||
or no such decision) rather than answering elsewhere.
|
||
- #797 / Gitea #1839 stage 1 (REQ-WEB-2), extending row 252 (#1753): an open
|
||
decision routed to the user offers one answer button per option, including
|
||
ordinary two-to-nine-option decisions with non-yes/no keys. A grant request
|
||
(`credential.mint`, raised with a grant and two options) retains Approve and
|
||
Deny with its concrete rights summary. Answers are enabled only with `--sso` and
|
||
`--send` and the page is signed in as a `console.sso` subject. The first click
|
||
POSTs `/api/decisions/<id>/confirmation` and shows what the answer does; only
|
||
Confirm POSTs `/resolve` with that one-use confirmation (two minutes, bound to
|
||
the session, business, decision, choice and current context digest). The broker
|
||
supplies authoritative `options` and a `kind` of `decision` or `grant`; grants
|
||
also retain `approve`, `deny` and `grant`. Ordinary HTTP confirmations carry
|
||
`kind: decision`, `action: answer` and the selected key/text, so the receipt
|
||
records that choice without claiming a grant started. Each step spawns
|
||
`packages/bus/src/console-cli.mjs`, which the bus verifies as the Console's
|
||
child holding the signed-in subject, and the bus records the answer `via
|
||
console` with a `human.input` naming that subject. Nothing retries; an unknown
|
||
outcome says to refresh. Cancellation, navigation and page reload discard the
|
||
page's confirmation; a refresh that finds a closed decision or a changed
|
||
context drops it too. The broker rechecks eligibility, digest and choice in
|
||
the resolve transaction. Missing or invalid grant bindings cannot fall through
|
||
to ordinary answers. Without `--sso` the page has no buttons and says why;
|
||
under `--sso` without `--send` it says the Console is read-only for decisions.
|
||
The tester listener has no decision routes. Decisions routed to agent roles
|
||
remain with those roles, using their real role/run attribution.
|
||
- The project remote module can display the configured HTTPS repository URL for
|
||
an owner session and save a missing URL when writes are enabled. A configured
|
||
URL is not verified and does not publish, set a Git
|
||
origin, grant credentials or contact a forge. Project information integration
|
||
supplies the authorized chat project to repository-key mapping.
|
||
- #797 / Gitea #1839 Task 3, the request intake panel (`src/public/requests.js`,
|
||
`src/request-http.mjs`): under each of the signed-in owner's latest three chat
|
||
messages that opened a request, the thread shows the request's phase, mode,
|
||
project and what it waits on, with buttons for what the owner decides. All of it
|
||
is escaped data (control and bidi characters show as `\uXXXX`). The panel reads
|
||
`GET /api/requests/<request>` (state) and `/projects` (the existing-project
|
||
picker, only the projects the bus authorizes for this request), re-reading on a
|
||
ten-second beat while a thread is shown, and never writes by mounting. It needs
|
||
`--sso`; its writes need `--send` too (otherwise the panel reads and says it is
|
||
read-only; `/api/config` reports `requests` and `requestWrites` to the owner
|
||
only). Five owner writes, each `POST` with a single same-origin `Origin`,
|
||
`Sec-Fetch-Site: same-origin`, `application/json`, a bounded body of exactly
|
||
these fields and a live session, spawning `console-cli.mjs` with the signed-in
|
||
subject (the page never names a business, subject or verb):
|
||
`/answer {revision, question, op, text}` (64 KB body, 8000 characters of text),
|
||
`/attach {revision, project, op}`, `/mode {revision, mode_revision, mode, op}`,
|
||
`/cancel {revision, op}`, and Task 4's `/revoke {project, snapshot, revision, reason, op}`.
|
||
The first four return the new request state; revoke returns a bound snapshot
|
||
revocation receipt. A bus refusal
|
||
maps to a named code and any unrecognised outcome is `outcome-unknown`, never
|
||
retried. Choosing the mode (new autonomous, new guided, or an existing project)
|
||
answers the selection decision through the same confirmation endpoints as the
|
||
Inbox (`/api/decisions/<id>/confirmation`, then `/resolve`). Picking an
|
||
existing project records the pick and the PM then stages a confirmation
|
||
decision; nothing is mapped until the owner confirms that. A text answer to the
|
||
PM's question is recorded as an attributed `human.input` linked to the question
|
||
and the request; it is never a PRD approval, and the original spec is not
|
||
rewritten. A mode change names the expected request and mode revisions and
|
||
is the bus's `request.mode.console` (the project's mode, shared by every request
|
||
attached to it): it withdraws a PRD approval waiting in the old mode and returns
|
||
each such request to discovery. A stale mode revision is `request-mode-revision-conflict`
|
||
(409). Each of answer, attach and cancel also needs the owner-to-PM messaging edge,
|
||
read when the control runs. Cancel asks
|
||
first, appends cancellation and keeps any project already created. An answer is
|
||
stored in `sessionStorage` before it is sent. After a lost response the page
|
||
reads the request first, and only the owner's click sends the stored answer
|
||
again with the same operation id, so the bus records it once. Sign-out clears
|
||
the panels and the stored answers. `GET /api/requests/<request>/draft` relays
|
||
the bus's `request.draft.console` reply (`candidate`, `prd`, `requirements`,
|
||
`snapshot`). Task 4 displays the exact PRD version, full PRD and requirement
|
||
text, selected paths/blob IDs, records commit, confirmed intent commit and mode
|
||
revision. Text is inert, including control/bidi characters. A missing or
|
||
mismatched draft offers no approval or revoke action. Guided PRD decisions
|
||
use the existing confirmation/resolve endpoints; their confirmation names the
|
||
displayed version and records commit. A changed candidate or request drops
|
||
the confirmation. Autonomous decisions stay with their eligible arbiter.
|
||
The panel shows the approved snapshot and offers **Revoke approved snapshot**
|
||
to a writable owner. Revocation requires a reason (1–1024 characters) and an
|
||
explicit confirmation; it keeps existing evidence and makes pending work wait
|
||
for replacement authority. The route checks project and snapshot against the
|
||
URL's current request before writing. Its revision is the draft read's
|
||
`snapshot.revision`, the project's latest snapshot-event counter, never the
|
||
request revision. The body limit is 4096 bytes. Stale revisions refuse 409
|
||
`request-revision-conflict`; a foreign snapshot refuses 403
|
||
`requirement-snapshot-refused`. A refusal is reported, and a lost or malformed
|
||
receipt remains unknown; neither retries. The page reads again after a revoke.
|
||
With no candidate (including after revocation), the draft read returns
|
||
`candidate: null`, `prd: null`, `requirements: []` and `snapshot: {id: null,
|
||
revision}`; revision is zero before the project's first snapshot event.
|
||
Changed selected PRD/requirement content refuses 409 `prd-candidate-stale`,
|
||
changed intent refuses 409 `stale-spec`, and a bus whose verb list lacks the
|
||
draft read refuses 503 `draft-unavailable`. These routes remain owner-only;
|
||
without SSO, request GETs refuse 503 and POSTs refuse 403. Sign-out clears the
|
||
draft panel and ignores late replies.
|
||
- What no Q1 verb returns is labelled where it would show, for example
|
||
"Bot names: not in the Q1 module" (lead decision 63). Nothing is guessed.
|
||
- A task's list row can only say its current state came from a Vikunja read,
|
||
since the list has no trail. The task page reads the trail and tells a task
|
||
the stack never created from one changed in Vikunja after the stack wrote it.
|
||
A stale poll read is shown in the snapshots and never as current.
|
||
- The task page's "Who may write" section lists each field with the verbs
|
||
that write it and the writer each verb checks: pm, the assigned role or any
|
||
role (row 61, lead decision 113). Whether a write also needs a resolved
|
||
decision is the role's authority, on the Business page. If the table read
|
||
fails, the section says why and the rest of the page still shows.
|
||
- The Tasks table has Requirement and Priority columns, from the `tasks` row's
|
||
`requirement` and `priority` (row 58, #1547). Either one shows "none" when
|
||
the row has none. A tombstone's page shows the priority of the last snapshot
|
||
that read the task.
|
||
- All bus- and agent-authored text is rendered inert: controls show as control
|
||
pictures or `[U+XXXX]`, and Markdown stays as source.
|
||
- Views refresh on the board's ten-second interval and stop on Pause. Focus,
|
||
open sections and the copy status survive a refresh.
|
||
- A failed read keeps the last good answer for that read and says it is old,
|
||
with the time it was read. A refusal (403, `not-configured`, `no-bus-host`,
|
||
or row 54's `queue-refused`) never shows kept rows (row 53), and it drops
|
||
every kept read: the Inbox count and the palette's decisions, tasks and
|
||
queue rows go too, until a read succeeds.
|
||
A page never read, or refused, shows the failure instead,
|
||
titled "Bus refused the read" (403: the bus refused the Console's read, for
|
||
example `human-required` from a Console started inside an agent run), "No bus to read" (no system
|
||
config, or no bus host and no `--business`) or "The read failed".
|
||
|
||
## Agent state (row 59, #1548)
|
||
|
||
`#/agents` adds State and Attention columns. State is the board's mark
|
||
(● working, ◐ waiting, – idle) from the bus `agents` verb, with the number
|
||
of messages and since when for a working role. Attention shows
|
||
"Input needed:" with the first line of the role's first open decision,
|
||
linked to `#/inbox/<id>`, a blocking chip, and "+N more" to the Inbox when
|
||
there are others. The page says what each state means. It writes nothing;
|
||
the server passes the rows through unchanged. How the bus derives the state
|
||
and its limits: `packages/bus/README.md`, "Agent state".
|
||
|
||
The same view has a separate **Thread claims** section (row 278, #1783).
|
||
It reads `thread.claims` and shows thread id, holder run, harness and claim time,
|
||
in the bus's order. Thread names, messages, instance and address are not shown.
|
||
An empty read has an empty-state line. A refused or failed read clears only
|
||
this section's kept claims and shows its reason; the held roles and their
|
||
count stay intact. Sign-out clears the claims with the other bus reads.
|
||
The read needs no `--send`. The bus permits human and reader sessions;
|
||
the tester listener refuses it with HTTP 403 `tester-refused` before any bus call.
|
||
|
||
## Board: held roles and signing out (row 95, #1589)
|
||
|
||
The Board (`#/`) opens with **Held roles**: each role a run holds on the
|
||
bus, from the same bus `agents` read as `#/agents`, including a claim with
|
||
no address ("no address"). Each row shows the role, its state and attention
|
||
(as in "Agent state"), the address, the holding run with its harness, and
|
||
since when. With send on (`--send`, "Message a role"), each row has a
|
||
Message action: it opens `#/inbox` with the send form addressed to that role
|
||
and the caret in the message. Without send there is no action, and the
|
||
section says so. A refused or failed first read says why and shows no roles
|
||
rather than a guess; a failed later read keeps the last roles under a stale
|
||
warning.
|
||
|
||
The machine scan's sessions (Waiting on you, Seen, All sessions and the
|
||
project list) sit behind **Show machine sessions**, off by default. While
|
||
it is off the page makes no `/api/board` request and polls nothing for it.
|
||
The choice is kept in this browser's local storage under
|
||
`mosaic-console-board` (`{"machine":true}`), like the appearance; where
|
||
storage is refused it starts off on each load. Turning it off drops the
|
||
board data and closes an open conversation.
|
||
|
||
Every Console fetch the page makes uses `redirect: 'manual'`. The Console
|
||
never redirects, so a redirect answer (`opaqueredirect`) is the sign-in
|
||
proxy's: its session has ended. The first one, on a board read, a bus read,
|
||
a page read or a send, signs the page out:
|
||
|
||
- One line says "Signed out. Reload the page to sign in." No NetworkError,
|
||
failure banner or stale warning is shown for it.
|
||
- Everything drawn from a Console read is removed (the board, the held
|
||
roles, the bus views, an open conversation and the inspector), because
|
||
none of it can be refreshed. A read that was in flight is dropped when it
|
||
lands.
|
||
- Refresh and Pause are disabled; nothing polls and no route change reads.
|
||
- The send form, if open, keeps its draft and receipt so the text isn't
|
||
lost; a further Send is refused in the page ("signed out"). A board
|
||
Reply that meets the redirect is not sent, and its inspector closes with
|
||
the rest of the board, so its draft isn't shown again.
|
||
- Reloading the page goes through the sign-in proxy again.
|
||
|
||
How long a sign-in lasts is the edge's setting (ops-01), not the Console's.
|
||
|
||
## Shell (row 53, #1542)
|
||
|
||
The Console is restyled to `docs/design` (`IMPLEMENTING.md`). It is a restyle
|
||
only: no new data, route or write.
|
||
|
||
- `src/public/tokens.css` and `src/public/icons.svg` are byte copies of
|
||
`docs/design/tokens.css` and `docs/design/icons.svg`; `tests/shell.test.mjs`
|
||
fails if either differs. Edit `docs/design`, rebuild there, then copy.
|
||
- Colours come from `tokens.css`, selected by `data-palette` and `data-mode` on
|
||
`<html>`. For the built-in default, Harbor, the page sets no colour inline. A
|
||
theme other than Harbor adds the fourteen colours of its module through the
|
||
CSSOM (`setProperty`), only after the page has checked each is `#rrggbb`; see
|
||
"Themes". Appearance System removes `data-mode`, so the OS light or dark
|
||
setting applies. The default is Harbor in the dark (`tests/shell.test.mjs`
|
||
reads `data-mode` under a dark OS, which cannot tell the two apart; an
|
||
earlier version of this line said System). The browser's choice is kept under
|
||
`mosaic-console-appearance:<organization id>`, or under
|
||
`mosaic-console-appearance` when the Console shows no organization.
|
||
- `shell.css` loads last and holds the restyle: radii `--r` 6px and `--r-lg`
|
||
10px, the type scale, tabular numbers, the command bar, section list, dense
|
||
table, inspector, status marks, class chips, toast, command palette and
|
||
not-found page. The earlier CSS files are unchanged.
|
||
- JetBrains Mono 400, 500 and 700 come from the official JetBrains release
|
||
(v2.304); `assets/fonts/jetbrains-mono-sources.txt` has the URL and sha256
|
||
of the archive and each file, and `jetbrains-mono-OFL.txt` the licence.
|
||
Manrope has no 800 file here, so 800 headings render at 700.
|
||
- Ctrl+K (or the search field) opens the palette over views, inbox decisions,
|
||
tasks and (row 54) queue rows from the last queue read; arrows move, Enter opens, Esc closes and returns focus. Arrow
|
||
keys also move between task links in the bus tables.
|
||
- Copy reports in a toast. If the clipboard is unavailable, the toast stays
|
||
until dismissed and the command is selected for Ctrl+C. When no toast shows,
|
||
the toast is an empty live region, visually hidden but still in the
|
||
accessibility tree, so the first copy is announced.
|
||
- The command bar shows, in UTC, when the board was scanned and the bus read,
|
||
and says when the last attempt failed. A board refusal drops the last scan
|
||
and everything drawn from it: the cards, the table, the inspector, the
|
||
project filter and tree, the counts, the footer and the scan line all say
|
||
the board was not read. A failed read with no scan held, after a refusal or
|
||
before the first scan, shows the board empty and says the read failed; it
|
||
never brings back rows from a refused scan. A refusal also closes an open
|
||
conversation and clears its history. If that removes what had focus, focus
|
||
moves to the page.
|
||
- From 760px the footer is a one-line status bar and the side column fits
|
||
between it and the command bar. Below 760px the section list is one strip
|
||
that scrolls inside itself.
|
||
|
||
## Themes (stream S4, organizations plan C5)
|
||
|
||
The ten palettes are installed `theme` plugins (`packages/plugins/theme/<name>/`;
|
||
the module contract is in `packages/plugins/README.md`). The Console lists the
|
||
folder on each request, so there is no list to keep.
|
||
|
||
- `GET /api/themes` answers `{themes:[{id, displayName, tokens:{light,dim,dark}}]}`
|
||
for the installed, valid themes, sorted by id. A refused module is left out
|
||
and logged once per reason. The route reads no body, header or query.
|
||
- Which theme shows, one field at a time (`theme`, `mode`): this browser's
|
||
override for the organization, then the organization file's
|
||
`appearance: {theme, mode}`, then Harbor in the dark. `mode` is `system`,
|
||
`light`, `dim` or `dark`. A missing `appearance` is valid.
|
||
- Clearing the browser's choice (Settings, Appearance) returns to the
|
||
organization's. The browser never writes the organization file.
|
||
- A saved theme that is not installed, or not valid, renders Harbor with a
|
||
banner that names it. The saved value and the browser's override are not
|
||
changed, so installing the module again brings the theme back. The Settings
|
||
picker shows it as a disabled "Not installed or not valid" tile.
|
||
- Whenever the organization's theme is not Harbor, a line under the command bar
|
||
names the organization, the theme and who chose it, so colour is never the
|
||
only cue.
|
||
- `window.MosaicAppearance.organization(id, saved, label)` is the call the
|
||
organization switcher makes when it changes the organization shown. Until
|
||
then the Console calls it once, from `/api/business`.
|
||
- The browser-wide record kept before this change (`mosaic-console-appearance`,
|
||
`{palette, mode}`) goes to the first organization this browser shows, once,
|
||
and is then removed. A record equal to the default (Harbor in the dark) is
|
||
not carried over: it cannot be told from a page nobody changed.
|
||
- Tests: `tests/appearance.test.mjs` (the rules), `tests/themes-http.test.mjs`
|
||
(the route), `tests/appearance-browser.test.mjs` (the page).
|
||
|
||
## Queue, Business and Settings (row 54, #1543)
|
||
|
||
Three read-only views, in the section list and the palette. Each has the
|
||
same five states as the bus views: loading (the skeleton names the source),
|
||
normal, empty, a failed read over kept data (labelled stale, with its time),
|
||
and a refusal that shows nothing kept. The command bar names the source:
|
||
"Queue read …", "Business not read: refused". A refusal shows the code in
|
||
mono and the module's own message, redacted by the server.
|
||
|
||
- `#/queue` lists every row with its state, owner, reviewers, brief and last
|
||
update, filterable by state. `#/queue/<id>` adds the gate, issues, claim,
|
||
`after`, note and review rounds. Rows move only with
|
||
`scripts/mosaic queue move` in a terminal; the row page shows that command
|
||
with a Copy button and has no button that moves a row. The brief is shown
|
||
as its path and anchor, because Console serves no repository files.
|
||
- `#/business` shows the business named by `--business`, else the running
|
||
bus host's business: the human, the arbiters as instance and kind ("pm
|
||
(delivery), cto (technical)"), projects, launch rules, the vars
|
||
on an allowlist, each role instance with its holder and harness, the
|
||
authority of each instance for each action as the organization package
|
||
classifies it (`resolveInstance`, `classify`), and credentials as
|
||
metadata only: service, account, role, expiry or rotate-by date, and a
|
||
state worked out from the date alone. A missing or invalid business file
|
||
is a `not-configured` refusal with the business module's own text.
|
||
- `#/settings` shows appearance (the row 53 control, kept in this browser),
|
||
the notifier binding by name only, and about: the Console's loopback
|
||
address, `RELEASE`, the business and the board URL. A Settings refusal
|
||
still shows appearance, because it needs no read.
|
||
|
||
Console never reads a token, a token file or a credential variable, and no
|
||
response, page or log carries one, a file path or a Discord channel, guild or
|
||
user id. The server redacts every message and string field before it answers
|
||
(`redactor` in `src/reads.mjs`): the config directory becomes `<config>`, the
|
||
data root `<dataRoot>`, any other absolute or `~/` path `<path>`, a 17 to 20
|
||
digit run `<id>`, and a JSON parse error's quote of the file is dropped. The
|
||
startup log goes through the same redactor. A path starts at the beginning
|
||
of the text or after a space, an opening bracket, a quote, `=`, `,`, `<` or
|
||
`:`, so `file:/x` and `file:///x` are redacted and `https://host/x` is not.
|
||
Known gap: a path stops at the first space, so a path containing a space is
|
||
redacted only up to that space and the rest shows. A bare `~` or `~user/`
|
||
is not treated as a path, and neither is a relative path.
|
||
|
||
## Spec and docs editor (row 148, #1642)
|
||
|
||
The Docs page (`#/docs`) lists, for each project of the business, spec/north-star.md and
|
||
the Markdown files directly under docs/. A file opens at `#/docs/<project>/<path>` with
|
||
its current commit. The page is a `<textarea>` that holds the Markdown source and a
|
||
preview beside it. Save writes exactly what was typed, as one local commit authored by
|
||
the business's human; the page then shows the new commit. Nothing is pushed.
|
||
|
||
Each save carries the commit the editor opened. If the file's last commit has moved on
|
||
since, the save refuses `stale-base`, nothing is written, and the page says to reload the
|
||
file and make the edit again. The draft stays in the box. Reload discards the draft and
|
||
reads the file again. A draft survives a refresh and a visit to another page; a file
|
||
with no unsaved change is read again when it is opened. The 10 s refresh is off on the
|
||
editor so it can't touch a draft. A file with a NUL character never opens: the project's
|
||
read refuses it (`project-read-content`), so the page has no draft to show. A file
|
||
with CRLF line ends saves with LF, and the page says so. The page does not create files
|
||
or open any other path. A tree with pending changes refuses the save (`project-dirty`).
|
||
|
||
The preview is built by `markdown-preview.js` with `createElement` and `textContent`
|
||
only. It never calls `innerHTML`, `outerHTML`, `insertAdjacentHTML` or `document.write`
|
||
(a test reads the file for all four), so it needs no sanitizer. Headings, paragraphs,
|
||
lists, block quotes, code blocks, inline code, emphasis and links render. Everything
|
||
else, raw HTML included, shows as the source text. A link is made only for an `http:`,
|
||
`https:` or relative target, with `rel="noopener noreferrer"`, and the target is shown
|
||
beside the link text. A target with a space, a control or bidirectional character, a
|
||
backslash or a leading `//` stays text. An image shows its alt text and URL and is
|
||
never loaded. Each render has a look-ahead budget of 32 characters examined per
|
||
character of text (at least 1 Mi): an opener with no closer searches at most 2000
|
||
characters, and the budget caps the total over a whole file. Once it is spent, every
|
||
later link, image, code span and emphasis shows as its source text, so a hostile file
|
||
can slow the editor by a fixed multiple of its length and no more. The preview vendors
|
||
nothing and the Content-Security-Policy is unchanged; the choice against a bundled editor
|
||
is recorded in `agents/researcher/work/queue-148/LIBRARY.md`. Row 154 later added one,
|
||
for the page's default view; this textarea and preview are now its Source view (see
|
||
"Visual editor").
|
||
|
||
Routes: GET `/api/project-files?project=` and GET `/api/project-file?project=&path=`
|
||
run the human transport like the other reads (400 for a bad query, 503 with no bus, 502
|
||
for an unusable answer). POST `/api/project-file` exists only under `--send` and has the
|
||
Settings boundary: the signed-in user's header, the exact `https://<public host>`
|
||
Origin, `Sec-Fetch-Site: same-origin`, a JSON body of at most 1 MiB with exactly the
|
||
string keys `project`, `path`, `base` and `content`. The 16 KiB limit of the message
|
||
route is unchanged. GET `/api/config` now also returns `edit`, true when saving is
|
||
available; without it the source is read-only and Save is off. An answer is
|
||
`{path, commit, at}`, a refusal `{error, message, sent:false}`, and an answer that is
|
||
lost or unusable is `sent:"unknown"`: the page keeps the draft, says the save may have
|
||
gone through, and sends nothing again. The log line names the project, path and
|
||
outcome, never the content.
|
||
|
||
Tests: `project-file.test.mjs` (the routes), `markdown-preview.test.mjs` (the sink
|
||
check, each element, the scheme rules, the look-ahead budget) and `docs-browser.test.mjs`
|
||
(markdown with `<script>`, `onerror` and `javascript:` runs nothing; the draft, each
|
||
save receipt, the stale-base message, sign-out and read-only mode). Those tests open the
|
||
Source view first (`source()` in the test file); their assertions are unchanged.
|
||
|
||
## Visual editor (row 154, #1648, Q168)
|
||
|
||
A file opens in a visual editor (ProseMirror) when the editor can write the file's own
|
||
bytes back with no edit; otherwise it opens in the Source view with a note. Visual and
|
||
Source are two buttons above the page. They edit one draft: a change in either shows in
|
||
the other, and the read, save, stale-base and read-only rules above are the same for
|
||
both. The editor is a view over the same `{project, path, base, content}` save.
|
||
|
||
The editor is built from reviewable source, not a prebuilt bundle:
|
||
|
||
- `third_party/editor/` is unmodified upstream TypeScript source of 19 packages
|
||
(ProseMirror, markdown-it and their dependencies), copied byte for byte from tagged
|
||
trees. Each package keeps its own license file and `package.json` beside its source;
|
||
`NOTICE` lists them, and `PROVENANCE.json` records the origin, tag, commit, upstream
|
||
tree and the git blob and sha256 of every file, the tree hash, and `localChanges`
|
||
(empty). `agents/researcher/work/queue-154/repro/import-sources.mjs` makes the
|
||
directory.
|
||
- `editor/` is the editor's own code: `model.mjs` (schema, parse, serialise), `view.mjs`
|
||
(toolbar, link bar, keys), `entry.mjs`, `editor.css` and `build.mjs`.
|
||
- `node editor/build.mjs` (`npm run build:editor`) bundles them with esbuild 0.28.2,
|
||
asserted exact, into `src/public/editor.js` and `editor.css`, one unminified script
|
||
with no source map, and writes `editor/BUILD.json` with the inputs and the sha256 of
|
||
each output. Two builds from different absolute paths give identical bytes. The built
|
||
files are committed, so a pinned copy needs no build step and no esbuild.
|
||
|
||
Rules the editor keeps from the preview: no raw HTML runs (it is kept as source text in a
|
||
tinted block), a link is made only for a target `window.MosaicMarkdown.linkTarget`
|
||
accepts (`http:`, `https:`, relative) and gets `rel="noopener noreferrer"`, and an image
|
||
is text (`[image: alt] url`), never an `<img>`, on the load path and on paste. The editor
|
||
adds no `innerHTML`-family call (a test reads `editor/*.mjs` for them; the bundle's
|
||
upstream code uses the DOM APIs and is not scanned) and no `eval`. The Content-Security-
|
||
Policy is unchanged: `editor.js` and `editor.css` are served from `'self'`.
|
||
|
||
Open and save with no edit writes the same bytes: the editor keeps each top-level block's
|
||
source and writes an untouched block back as it was. A block you change is written in
|
||
the editor's own form (`-` bullets, soft breaks as new lines, reference links as inline
|
||
links, `<` and `&` escaped). Tables, HTML blocks, front matter, indented code, setext
|
||
headings, strikethrough, link reference definitions, and a block that holds a link target
|
||
the gate refuses are kept as source blocks and not edited visually. Known limits: CRLF
|
||
saves as LF (as in row 148); an unedited file keeps Save off; an empty paragraph is
|
||
dropped when its neighbour is edited; copying an image inside the editor does not keep
|
||
it; two adjacent lists may merge when the file is read again; a redraw or a switch
|
||
between Visual and Source starts a new undo history.
|
||
|
||
Routes: GET `/editor.js` and GET `/editor.css`, static like the other public files.
|
||
|
||
Tests: `editor.test.mjs` (routes, the round trip over the goals review, a document with
|
||
every element and every repository doc, the source rules), `editor-provenance.test.mjs`
|
||
(the imported tree against `PROVENANCE.json` and its recorded local changes, license
|
||
files, the build record, and a rebuild that skips with a reason when esbuild 0.28.2 is
|
||
not installed) and `editor-browser.test.mjs` (hostile content on the load and the paste
|
||
path, the Console's CSP, edit and save, the link bar, read-only).
|
||
|
||
## Assistant settings (row 155, #1649)
|
||
|
||
Jason's ruling Q169 lets the human configure the assistant and its seat name from the
|
||
Console. Settings shows an Assistant section with one form per assistant instance, the
|
||
ones the Settings read lists under `assistants`:
|
||
|
||
- the seat name, read through `/api/bus/names`, with Save name. If the read fails the
|
||
section says the current name could not be read, and the field starts empty;
|
||
- `definition`, `vars` and `tracker` as text, the last two as JSON, with Save assistant
|
||
settings. It sends only the fields that changed, under the revision the page read, as
|
||
POST `/api/settings` with `assistants` (the business README's "Assistant settings"). An
|
||
empty tracker sends `null`, which removes it. `vars` and `tracker` that aren't JSON
|
||
objects are refused in the page and nothing is sent;
|
||
- Stop its runs, which ends every run of that assistant in the threads the human made.
|
||
|
||
Without `--send` every control is off. A refusal shows the bus's message and keeps the
|
||
draft; a lost answer says the change may have gone through. Nothing is sent again on its
|
||
own.
|
||
|
||
Routes: POST `/api/name` with exactly the string keys `instance` and `name` calls
|
||
`name.set`, and POST `/api/assistant-stop` with exactly `instance` calls `assistant.stop`.
|
||
Both exist only under `--send`, go through the human transport, and have the Settings
|
||
boundary: the signed-in user's header (401 without it), the exact `https://<public host>`
|
||
Origin and `Sec-Fetch-Site: same-origin` (403), and a JSON object of at most 4 KiB (400).
|
||
Any other method is 405. An answer is the bus's result with `at`. A bus refusal is
|
||
`{error, message, sent:false}` with the code's status, `not-held` and `name-taken` 409 and
|
||
`human-required` 403. A lost or unusable answer is 503 with `sent:"unknown"`. The log line
|
||
names the route, the user and the instance, and the outcome: `done` or the code. A Console
|
||
started inside an agent run gets `human-required` from the broker.
|
||
|
||
## Role instances (row 165, #1662)
|
||
|
||
Q171 (REQ-ROLE-3) lets the human add a role instance and change one's definition, vars and
|
||
launch membership from the Console. Settings shows a Role instances section, read through
|
||
`/api/bus/instances`: a table of each instance with its definition, whether it is launched
|
||
(or is the launcher) and its vars, then two forms.
|
||
|
||
- Add an instance: a name, a definition from the ones the writer accepts, vars as JSON and,
|
||
when the file has a launch block, a Launch it box.
|
||
- Change an instance: any instance except an assistant, which changes in the Assistant
|
||
section. The definition, the vars (replacing them whole) and launch can change; a field
|
||
left as Keep or empty is not sent.
|
||
|
||
Definitions that declare credentials are not offered. The section names them, with their
|
||
services, as hand edits of the business file, because the Console binds no credentials.
|
||
Each form sends one POST `/api/instances` under the revision the page read. When the answer
|
||
says a bus restart is needed, the receipt says so and that the Console does not restart it;
|
||
the page has no restart control. Without `--send` every control is off. A
|
||
refusal shows the code and the reason and keeps the draft; a lost answer says to read the
|
||
instances again. Nothing is sent again on its own.
|
||
|
||
Route: POST `/api/instances` exists only under `--send` and calls the human-only
|
||
`instances.set` through the human transport. It has the Settings boundary: the signed-in
|
||
user's header (401 without it), the exact `https://<public host>` Origin and
|
||
`Sec-Fetch-Site: same-origin` (403), and a JSON object of at most 16 KiB (400). It takes
|
||
exactly `revision` (64 hex), `instance`, `op` (`add` or `change`) and any of `definition`,
|
||
`vars` (an object) and `launch` (a boolean); any other key, `holder`, `credentials` and
|
||
`tracker` included, is 400 before the bus. Any other method is 405. An answer is the bus's
|
||
result with `at`. The bus sends a refusal's code alone, so the route adds the reason:
|
||
`{error, message, sent:false}`, where the message is the code, the reason and "Nothing
|
||
changed." `revision-conflict` and `organization-instance-exists` are 409,
|
||
`human-required` 403 and the other refusals 400. A lost or unusable answer is 503 with
|
||
`sent:"unknown"`. The log line names the user, the instance and the outcome (`done
|
||
restart=true|false` or the code), never the vars.
|
||
|
||
## Routing (row 65, #1558, mos-0840)
|
||
|
||
Settings shows a Routing section, read through `/api/bus/routes`: a table
|
||
with one row per instance in the business file and six selects, harness,
|
||
model, thinking, fallback harness, fallback model and fallback thinking.
|
||
It replaces the Agent limits form's old Role models fieldset, which set
|
||
only the model. The harness cells offer the harnesses `plugins.harness`
|
||
switches on, the model cells the `launch.models` keys, and each thinking
|
||
cell the levels its row's harness applies. A value the file holds that
|
||
isn't offered stays selected and says why: "(not switched on)", "(not in
|
||
the model table)" or "(not applied by <harness>)". A harness change redraws
|
||
the row, so its thinking cell offers the new harness's levels. With no
|
||
harness switched on, the section says every launch refuses `harness-off`
|
||
and that `mosaic plugins set` switches one on; the page doesn't change
|
||
`plugins`.
|
||
|
||
A fallback is set or cleared as a whole. Save sends one POST `/api/routes`
|
||
under the revision the page read, with only the changed rows. A fallback
|
||
whose three cells are `none` is sent as null, which clears it; one with
|
||
some cells set goes as it is, and the bus refuses it as partial. A
|
||
submit with nothing changed says "Nothing changed." and sends nothing. A
|
||
save says which instances it saved and that the next launch of each uses
|
||
the new route; running sessions keep theirs. A refusal shows the code and
|
||
the reason and keeps the draft. A lost answer says the outcome is unknown
|
||
and to read the routes again. "Read current routes and discard draft"
|
||
drops the draft. Without `--send` every select and Save are off. Nothing
|
||
is sent again on its own.
|
||
|
||
Routes:
|
||
|
||
- GET `/api/bus/routes` calls the human-only `routes` through the human
|
||
transport. A query string is 400, no bus 503, and an answer without a
|
||
64-hex revision, the `harnesses` and `models` lists and the `levels` and
|
||
`instances` objects 502.
|
||
- POST `/api/routes` exists only under `--send` and calls the human-only
|
||
`routes.set`. It has the Settings boundary: the signed-in user's header
|
||
(401 without it), the exact `https://<public host>` Origin and
|
||
`Sec-Fetch-Site: same-origin` (403), and a JSON object of at most 16 KiB
|
||
(400). It takes exactly `revision` (64 hex) and a non-empty `routes`
|
||
object; any other key is 400 before the bus. Any other method is 405.
|
||
The bus sends a refusal's code alone, so the route adds the reason:
|
||
`{error, message, sent:false}`, where the message is the code, the reason
|
||
and "Nothing changed." `revision-conflict` is 409, `human-required` 403,
|
||
and `harness-off`, `harness-refused`, `thinking-unsupported`,
|
||
`unknown-model`, `organization-partial-fallback`,
|
||
`organization-unknown-instance`, `organization-no-change` and
|
||
`invalid-request` 400. A lost or unusable answer is 503 with
|
||
`sent:"unknown"`. The log line names the user, the instances and the
|
||
outcome (`done` or the code), never the routes.
|
||
|
||
Neither route is in `organization.set`'s keys, so the chief of staff can't
|
||
reach them (`packages/organization/README.md`, "Routes").
|
||
|
||
## Data and boundaries
|
||
|
||
GET `/api/board`, `/api/conversations` and `/api/conversation`, and POST
|
||
`/api/seen` and `/api/reply` proxy only those board paths. Only the two
|
||
conversation routes carry their query string; the board validates it. POST
|
||
bytes and upstream status/JSON are preserved. GET `/api/config` returns the
|
||
configured board URL for the page's error message. No scanner, registration,
|
||
session reader or transport is implemented here. The session reader is
|
||
`packages/conversation`, served by the board.
|
||
|
||
GET `/api/bus/inbox`, `/api/bus/tasks`, `/api/bus/agents`,
|
||
`/api/bus/trail?subject=<id>`, `/api/bus/decisions?list=roles|recent`,
|
||
`/api/bus/credentials`, `/api/bus/names` (row 155, each instance's seat name),
|
||
`/api/bus/instances` (row 165, the role instances and the definitions `add` accepts),
|
||
`/api/bus/replies?id=<message id>` and
|
||
`/api/bus/messages?limit=50&after=<message id>` are the bus
|
||
reads (`src/bus.mjs`). Any other method is 405. The subject and the id must
|
||
match the broker's identifier rule, and the list must be `roles` or
|
||
`recent`, else 400. `replies` (row 81, #1575) is the messages whose
|
||
`in_reply_to` is that id, for the Sent receipt; the bus refuses an id that
|
||
names no message (`message-not-found`, 400). `decisions` (row 57, #1546) is for the Inbox tabs "Routed to roles" and
|
||
"Recent"; "Human and reader paths" in `packages/bus/README.md` has what each
|
||
list holds. `credentials` (row 60, #1549) is the bus host's credential
|
||
status: service, account, role, date and state for each credential it
|
||
loaded, never a token, file path or variable name. No page uses it yet; the
|
||
Business view still shows the business file's metadata.
|
||
Each read runs the S4 human transport (`packages/bus/src/human-cli.mjs`) as
|
||
a child process, so a slow read doesn't stall the server. The bus checks
|
||
that child's ancestry, which ends at the Console process; it never sees the
|
||
browser or any other client of the port. So while the Console runs,
|
||
anything that can connect to its port (7330 by default) reads what a reader
|
||
capability reads ("Human and reader paths" in `packages/bus/README.md`):
|
||
the inbox, tasks, agents, trails, the two decision lists and the credential
|
||
status. That includes
|
||
a T3 seat using `curl` and a managed S6 session, since S6 doesn't confine
|
||
the network. Without `--send` no route
|
||
writes (Q4), so the exposure is reads only; with it, see "Message a role". Slice 1 doesn't change this
|
||
(Filbert F1 on #1522). Answers are `{ rows, at }`; failures are `{ error, message }`
|
||
with 403 for a refusal, 503 for no bus or no answer, and 502 for an answer
|
||
that can't be used. The bus needs the system config
|
||
(`~/.config/mosaic-dev/config.json`); without it the board still serves and
|
||
the bus routes answer `not-configured`.
|
||
|
||
GET `/api/queue`, `/api/queue/<id>`, `/api/business` and `/api/settings` are
|
||
row 54's reads (`src/reads.mjs`), under the same Host, Origin and CSP rules.
|
||
Any other method is 405, and a queue id that is not a positive integer is
|
||
400. The queue is read in a child process (`src/queue-read.mjs`) through
|
||
`rows()` from `packages/queue/src/store.mjs` (lead decision 83): the same
|
||
read as `queue list`, with no write and no network. Like `list`, it takes
|
||
the queue lock only to recheck a disagreement before reporting it. The child
|
||
imports the store inside its `try`, so a store that fails to load reports
|
||
"the queue read failed". Only the store's exit 2 (invalid data or refused)
|
||
forwards the child's text, as `queue-refused`; any other exit is the fixed
|
||
`read-failed` message "the queue read failed (exit N)", because that stderr
|
||
may be Node's own, with a `file://` path and a stack. The business
|
||
read uses `packages/organization` in process. No read writes, calls out or adds a
|
||
dependency. Answers carry `at`; failures are `{ error, message }` with 503
|
||
for `not-configured`, `no-bus-host`, `queue-refused` and `read-failed`, 404
|
||
for a row that isn't there, 400 for a bad address and 502 otherwise. Settings
|
||
answers 200 even without a system config, so appearance always works; its
|
||
notifier part then says `not-configured`.
|
||
|
||
GET `/api/task-fields` is row 61's read (#1550): the task field table,
|
||
`packages/tasks/fields.json`, loaded and checked by `loadFields()` on each
|
||
request. It is repository data, so it answers without `--business` or a
|
||
system config. The answer is the table itself, with no `at`. A table that
|
||
fails its check is refused whole with 503 `read-failed` and the loader's
|
||
message, which names the file or the key at fault, never a path. Any other method is 405.
|
||
|
||
Console's shared CSS, Console CSS, brand.js and local Manrope fonts were copied
|
||
unchanged from `agents/dewey/work/wui/`. Font license and source URLs accompany
|
||
the files under `src/public/assets/fonts/`. `live.css` contains the live-page
|
||
adaptations; original mockups and unrelated pending design work stay untouched.
|
||
Unsupported mockup fields and controls are listed in `docs/plans/DEFERRED.md`.
|
||
The source tree is a project filter; no invented workspace registration tree
|
||
or mockup session hierarchy is presented as live data.
|
||
|
||
## Verify
|
||
|
||
```sh
|
||
node --test packages/webui/tests/
|
||
node --test packages/control-board/tests/ packages/seat/tests/ packages/ledger/tests/ packages/mosaic/tests/
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/browser.test.mjs
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/conversation.test.mjs
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/bus-browser.test.mjs
|
||
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/shell.test.mjs
|
||
node --test packages/webui/tests/reads.test.mjs packages/webui/tests/views.test.mjs
|
||
```
|
||
|
||
Node's test runner and installed `/usr/bin/chromium` are required. Set `CHROMIUM`
|
||
to another installed Chromium path. No npm download is needed. The copied CDP
|
||
helper starts a separate temporary browser profile and removes it on exit.
|
||
Tests use temporary board session/registration files and stub the board's
|
||
transport. They never send to real seats or read real session logs.
|
||
|
||
Browser tests exercise the rendered real board fixture, exact counts, project
|
||
filtering, keyboard return focus, Seen/Unsee, success/failure receipts, draft and
|
||
caret preservation, pending-send exclusion, storage denial, stale registration,
|
||
loading/empty/malformed/unreachable states and hostile text. Contrast is measured
|
||
on 330 rendered samples across ten palettes and three modes. Layouts are checked
|
||
at 320, 390, 768, 1440 and 2560px. Horizontal scrolling is intentional within the
|
||
dense table; the page itself must not overflow.
|
||
|
||
Conversation tests run the real board routes over temporary repository-layout
|
||
session files. They cover full-length answers, collapsed tools and thinking,
|
||
hostile content rendered inert, malformed-line and reconcile markers, forks, and
|
||
the return flow: a send from the view, a tool call and a delayed result while a
|
||
draft is typed, a peer message, a 4.5-million-character answer split into
|
||
continuation parts, and a relaunch mid-turn.
|
||
|
||
Row 95's tests (`board-held.test.mjs`) seed an in-process broker with one
|
||
claim that has an address and one that has none. They check the held roles
|
||
with the machine sessions off by default and no `/api/board` request, the
|
||
toggle on and off and its persistence across reloads, Message addressing
|
||
the send form to each role, no action without send, and that every fetch
|
||
asked for `redirect: 'manual'`. The tests record every 10 s poll timer the
|
||
page arms, filed by the script that armed it, and assert that none is armed
|
||
for the board while the toggle is off, even across Pause and Resume, and that
|
||
one is armed once it is on. Then a redirect answer on each of six
|
||
requests, the board read (`/api/board`), the `agents` read, `/api/conversations`,
|
||
`/api/seen`, `/api/queue` and the send: one line, no NetworkError, nothing
|
||
stale left, and no further request or new poll timer on a route change,
|
||
Refresh, the toggle, a forced Pause click or a poll tick, and the draft kept. In
|
||
that test the poll timers fire after 150 ms, so the check spans several
|
||
ticks. `browser-edge.test.mjs` fails in 5 s, and closes the browser and
|
||
servers, if the toggle makes no `/api/board` request. `send-browser.test.mjs`
|
||
also checks the sign-out after its redirect receipt and that a reload works
|
||
again. Since the held roles lead the Board,
|
||
the shell test's no-scroll check at 1440px uses a 1000px-tall window.
|
||
|
||
Bus tests (`bus.test.mjs`, `bus-browser.test.mjs`) run the routes and the
|
||
views against an in-process broker read through its reader session, so every
|
||
row has the broker's own shape. `humanCall` runs against a stub CLI script.
|
||
They cover each view, the gap labels, inert hostile text, Copy, the stale
|
||
fallback and each failure title, and that no route writes. They never start
|
||
the human CLI against a live bus.
|
||
|
||
Shell tests (`shell.test.mjs`) check the copies, the font hashes and the kept
|
||
strings, and in the browser at 400px: System mode under an emulated dark and
|
||
light OS, the empty, stale and refused states of the board and the bus views
|
||
(a failed first read drawn empty, a board refusal with the inspector open and a project picked, then a failed
|
||
read, a failed session list, and a refusal with a conversation open and where
|
||
focus lands after it), not found, the palette, the toast,
|
||
keyboard and visible focus, the section strip at 360 and 400px, no sideways
|
||
scroll, no script error and no request other than GET. A second test walks
|
||
the Board, Inbox, Tasks, Agents and a decision trail through loading (a held
|
||
read), normal, a failed read over kept rows, and a refusal by the bus
|
||
(`human-required`) and for want of one (`no-bus-host`). It then checks that a
|
||
refusal empties the palette and the Inbox count, from a view, from the
|
||
palette's own reads and from the board page, and that palette labels are text.
|
||
|
||
Row 54's tests share the seeded fixture in `reads-fixture.mjs`: a scratch
|
||
queue whose row note names an id and token paths shaped like real notes
|
||
(absolute, `~/` and after `file:`; the store refuses `<` in a note, so the
|
||
`<…>` case is in the redactor test), and a business,
|
||
notifier config and Discord binding that hold token files, an environment
|
||
variable name, an agent `model` var holding a path, and guild, channel, bot
|
||
and user ids. Row 9's After entry has the `{id, when}` shape the real queue
|
||
stores. `reads.test.mjs` checks each
|
||
route's body and status, the redactor, the credential state, methods and
|
||
Origin, and that no body carries any seeded value or the temporary directory;
|
||
that a `store.mjs` with a syntax error, or none, gives the fixed message with
|
||
no path; and that the child's timeout, output cap and non-2 exits hold, over a
|
||
fake child script.
|
||
`views.test.mjs` walks Queue, a queue row, Business and Settings through the
|
||
five states over a stub read, with each refusal code the view can meet, at
|
||
400px; checks that a refusal leaves no queue row in the palette, that no view
|
||
has a control but Copy and Read again, and that the row page shows the
|
||
`queue move` command, that the 10 s refresh keeps focus on a Settings mirror
|
||
select, and that the palette keeps its queue rows after its own inbox and tasks
|
||
reads answer; then, over the seeded fixture, that the Arbiters line reads the
|
||
business file's object, that neither the page nor
|
||
any response it read carries a seeded value, that a missing or invalid
|
||
business file shows the module's text, and that the startup log names no
|
||
config path.
|
||
|
||
`assistant.test.mjs` (row 155) checks both seat routes: the answer and the log line,
|
||
`--send` off, the Host, user, Origin and Fetch Metadata checks, GET, strict keys, the size
|
||
and content type, each refusal's status, the lost answer and the `names` read.
|
||
`settings-write.test.mjs` checks that an `assistants` patch reaches the human operation
|
||
unchanged and that the log names the user. `settings-form.test.mjs` walks the Assistant
|
||
section in a browser: off without `--send`, Nothing changed, only the changed field sent,
|
||
a client-side refusal, a refused post that keeps the draft with no retry, Save name and
|
||
Stop.
|
||
|
||
No root CI workflow is configured for this package. These local tests are not a
|
||
claim of CI, deployment, live-seat delivery or user acceptance.
|
||
|
||
## User test and rollback
|
||
|
||
Gate E belongs to Jason on Monday 2026-09-14. Use Console to find who is waiting,
|
||
open a registered repo seat, reply and see its next answer after a scan. Mark a
|
||
completion Seen and find it again in Seen. Record any reason to open the board's
|
||
own page or a repo seat terminal in DEFERRED.md. Pass is Jason's end-of-day
|
||
say-so, not a test result. Keep #1507 open pending that ruling.
|
||
|
||
Rollback requires only stopping the WebUI and using the unchanged board at 7331.
|
||
Revert the scoped WebUI commit to remove it; there is no data migration to undo.
|
||
Seen and reply actions already taken belong to the board and are not rolled back.
|
||
|
||
## Messages (row 94, #1588)
|
||
|
||
The Messages page shows every message addressed to the human, newest first,
|
||
including unsolicited messages and late replies. Each lists sender, class,
|
||
time, read state, literal body and a trail link. It polls at the existing
|
||
10-second cadence; pause and refresh work as on the other pages. Older
|
||
messages and Newest messages links page through the history. The nav count
|
||
is the whole feed's unread count, including messages beyond the visible
|
||
page. If that count cannot be read, it is hidden; a bus refusal clears kept
|
||
data as on the other pages. Reading this page marks nothing delivered or
|
||
read. `mosaic follow` or `mosaic talk --follow` prints messages and marks
|
||
them read in the terminal. The Console does not consume them.
|
||
|
||
The messages HTTP read accepts only limit (1–200, default 50) and after
|
||
(a message id cursor); malformed, duplicate or unknown query keys refuse
|
||
400 invalid-request. It returns rows plus total, unread, next and the
|
||
Console read time at. Older read routes retain their rows/at response.
|
||
The broker owns business scoping and cursor authorization.
|
||
|
||
`messages.test.mjs` exercises the route's paging/refusals and existing
|
||
route response shapes, then the page against a temporary broker with
|
||
a scheduled poll accelerated in the browser test. It proves literal
|
||
content, late unsolicited arrival, pagination, full unread count and
|
||
unchanged delivery receipts. No live bus data or service is used.
|
||
|
||
## Projects, requests and files (#797)
|
||
|
||
Projects opens the project overview, its conversations and project information.
|
||
A conversation retains its exact session in the URL. The navigation drawer shows
|
||
context from the current authorized read. Sidebar display preferences stay in
|
||
this browser. Inbox cards show the recorded decision origin and receipt details.
|
||
|
||
Request panels show known request state and reviewed delivery evidence. Their
|
||
Files area and the conversation Files area read only explicitly observed request
|
||
IDs. Draft workspace files are names only; reviewed delivery files retain their
|
||
existing open and download controls. Missing or refused reads remain unavailable.
|
||
|
||
Project information can show a linked repository's configured URL or, when the
|
||
owner has write access, save a missing HTTPS URL. Saving configures the URL only;
|
||
it does not verify or publish it. The server rechecks authorization and refuses
|
||
replacement of an existing URL. Granted users and tester transports do not gain
|
||
these owner controls.
|
||
|
||
## Chat projects and threads (row 129, #1623)
|
||
|
||
Open **Chat** in the section navigation or command palette. The business project
|
||
catalog is shared; each project's thread list includes only the current human's
|
||
threads. Create a named project, optionally linked to a configured repository
|
||
key, then create a named thread. Names are set at creation; rename is not shipped.
|
||
No default project or thread is created implicitly. A thread has its own URL,
|
||
draft and conversation. This is separate from the Board's seat conversations.
|
||
|
||
The routes are `#/chat`, `#/chat/projects/UUID` and `#/chat/threads/UUID`.
|
||
The fixed `/api/chat/projects`, `/api/chat/threads` and
|
||
`/api/chat/threads/UUID/messages` endpoints list/create projects, list/create
|
||
threads and read/send messages. `POST /api/chat/threads/UUID/open` is the explicit
|
||
**Resume assistant** action. Reading a thread never opens it or acknowledges a
|
||
message. Sending uses `thread.send` with class REQUEST, as the human CLI does.
|
||
The owner's archive controls (#797) live in `public/project-archive.js` and
|
||
`src/project-archive-http.mjs`: a **Show archived** toggle on the project list,
|
||
Archive (with a confirm) on a project, and Unarchive on archived ones, over
|
||
`GET /api/chat/projects/archived` and `POST /api/chat/projects/UUID/archive|unarchive`,
|
||
fenced exactly like the chat writes above.
|
||
|
||
All four POST operations need `--sso --send`, the configured public Host, a
|
||
valid OIDC session, the Console's HTTPS Origin, same-origin Fetch
|
||
Metadata and a strict JSON object within 16 KiB. The broker binds the business
|
||
human and enforces ownership. A same-business thread owned by someone else
|
||
returns 403 `thread-not-owned`; a missing/foreign thread returns 404. No browser
|
||
owner, business, capability, arbitrary bus verb or filesystem path is accepted.
|
||
Read-only mode disables creation, Resume and Send. Chat uses the existing human
|
||
transport; a later channel-module conversion has its own brief (sage-0724).
|
||
|
||
History starts at the oldest message, reads forward 100 at a time and retains a
|
||
bounded 200-message display window. **Read next messages** advances older
|
||
history; a full page means more may exist. Once caught up, polling uses the last
|
||
id and de-duplicates replies. An accepted receipt confirms the message only,
|
||
not that the assistant has answered. An uncertain result never retries: read
|
||
the list/history before explicitly trying again. Drafts are memory-only and
|
||
thread-scoped; sign-out clears private chat state. Bodies are inert text.
|
||
|
||
### Tester listener
|
||
|
||
`serve --tester-socket` serves tester HTTP at `<dataRoot>/console/tester.sock`.
|
||
It creates or checks console/ as 0700 and the socket as 0600, owned by the instance
|
||
uid. It refuses symlinks, unsafe modes, existing entries and overlong Unix paths.
|
||
It binds a unique temporary name in that parent and renames onto the final path.
|
||
Cleanup removes the final path only when its dev/ino still matches. Hostile
|
||
same-uid replacement races remain outside the boundary.
|
||
|
||
There is no tester TCP listener. `--tester-port` is retired for this copy.
|
||
Mos accepted loss of direct browser navigation (mos-1015). Agents use the socket:
|
||
|
||
```sh
|
||
node packages/cli/src/tester-http.mjs GET /api/config
|
||
curl --unix-socket "$INSTANCE_DATA/console/tester.sock" http://mosaic-tester.invalid/healthz
|
||
```
|
||
|
||
The helper selects dataRoot from the instance config, uses fixed Host and Origin
|
||
`http://mosaic-tester.invalid`, and adds same-origin Fetch Metadata and JSON for
|
||
POST bodies read from stdin. It never retries an uncertain write. Ordinary
|
||
browsers cannot navigate this socket. A later authenticated browser relay behind
|
||
SSO requires its own row.
|
||
|
||
The tester calls TESTER_CLI for reads, instance/route/name writes and its own chat.
|
||
It rejects sign-in cookies, Authorization, proxy auth/proof headers and owner-only
|
||
writes. Its bus calls still require `bus.tester` exactly true. A tester cannot
|
||
become an owner or user. The main listener rejects x-mosaic-tester for every Host.
|
||
Credential and committed-project read restrictions remain in force.
|
||
|
||
In an authorized window, `stack-release.sh activate ID BUSINESS --tester-socket`
|
||
explicitly converts inherited tester TCP flags in both unit and pointer. Without
|
||
that flag a new target refuses inherited TCP before stopping any unit. Rollback
|
||
to an older target drops tester enablement and reports it, without reopening TCP.
|
||
The reviewed window tool must also be used for rollback. Failed activation may
|
||
restore the old saved TCP unit, which restores the old security state and must
|
||
be reported as such. Builds and tests never change live units, listeners or tunnels.
|
||
|
||
### Proxy proof (row 178)
|
||
|
||
Preview mode can additionally require `--proof-header NAME --proof-file PATH`.
|
||
Both flags go together and require the three preview flags. The file must be a
|
||
regular, non-symlink file owned by the service uid, mode 0600 or 0400, containing
|
||
one line of at least 32 base64url characters (`A-Z`, `a-z`, `0-9`, `_`, `-`),
|
||
with at most one final LF. Serve reads it once before listening. Only the header
|
||
name and file path go on argv; the value is never printed, returned in config,
|
||
or logged. Restarting with the new file is how an operator rotates the proof.
|
||
|
||
For the public Host, exactly one proof header must match before the auth header
|
||
is read. Missing, repeated or incorrect proof gets the same 401 `proxy proof
|
||
required` and reaches neither bus nor board. The proof header with any other
|
||
Host, including the tester listener, gets 403. Without the flags startup says
|
||
`proxy proof off`; with them it says `proxy proof on`.
|
||
|
||
`webui-service.sh render` accepts the pair, checks file metadata and refuses
|
||
paths inside the checkout or configured copies directory. It never opens the
|
||
file. `stack-release.sh` carries the pair unchanged across re-pin and rollback.
|
||
|
||
This is a partial boundary. It blocks a caller that cannot read the file; it
|
||
cannot block another process running as the Console uid. Separate service uid
|
||
is Jason's remaining infrastructure decision on #385. No live proof, middleware
|
||
or unit flags are installed by this code change. Activation is a separately
|
||
plan-checked Mos window: proxy middleware first, then Console flags and restart;
|
||
rollback removes flags before middleware. Do not reuse console-allowed-uid,
|
||
which is an identity rather than a proxy secret.
|
||
|
||
## Instance processing controls
|
||
|
||
The persistent Stop processing control and halted banner sit outside the
|
||
route-specific views. GET /api/processing remains read-only. POST
|
||
/api/processing/halt and /api/processing/resume use the existing human writer,
|
||
`--send`, named same-origin checks and bounded strict JSON; a tester cannot
|
||
write them. Resume asks for confirmation and submits the displayed generation.
|
||
Unknown outcomes stay explicit and no write is retried.
|
||
|
||
During Q175's read-only interim both buttons show as unavailable and point to
|
||
`mosaic halt` / `mosaic resume` in the instance CLI. There is no halt-only send
|
||
exception or alternate authentication path. Landing the UI and activating it
|
||
remain separately gated.
|
||
|
||
Row 208 (193d): Chat includes a **Shared with me** list. Shared threads are
|
||
read-only, including for the business owner. On your own writable threads,
|
||
**Share read-only** accepts `user:ID`, the business owner's name, or `*` for
|
||
everyone with coverage; **Unshare** ends that target's share. A share grants
|
||
no project or instance access of its own.
|
||
|
||
Viewers can read shared threads and their old own threads. Their pages omit
|
||
creation, composer, resume, share and session mutation controls. Mixed grants
|
||
apply separately to each project and instance; a collaborator grant elsewhere
|
||
does not enable writes here. Grant hints refresh on each user chat render and
|
||
background refresh, while the bus decides every operation from live grants.
|
||
The existing user notice names the governance records visible to the owner.
|
||
|
||
## Model proxy page (P4, #1828)
|
||
|
||
The owner's Settings → Model proxy page (`/settings/model-proxy`) signs in Claude/Codex accounts, accepts write-only
|
||
z.ai API keys, sets account priority, and lists client-key IDs. It uses only
|
||
`/api/proxy/` routes over the owner's human bus transport. The old `/proxy` bookmark redirects to this page after the same access checks. A missing instance
|
||
proxy refuses `not-configured`; it does not ask for a management key. That key
|
||
stays in the bus. No panel HTML or proxy-port forward is served.
|
||
|
||
The page and routes refuse granted users, tester listeners and loopback Hosts.
|
||
Writes also require `--send`, the Console's own Origin and same-origin Fetch
|
||
Metadata. Account display names use a provider label and stable hashed id;
|
||
arbitrary upstream labels and emails are not reflected. Key inputs and pasted
|
||
callback URLs are cleared when submitted and never saved in browser storage.
|
||
Only an `ok` status completes OAuth; accepted callbacks remain pending. At the
|
||
bus's default300-second timeout, status returns `error/login-timeout` once and
|
||
retires the state. Refreshing the page does not restore a pending login.
|
||
|
||
Usage is reserved on this page for P5; P4 adds no usage source. PIN changes
|
||
require the offline upgrade acceptance documented in packages/model-proxy.
|
||
|
||
Archived project routes use the inclusive project read for the name, saved threads and room history. They offer Unarchive in send mode; thread creation, resume, sending and clear controls are unavailable while archived. Archive styling uses only `.project-archive-control` and `.project-archived-filter`.
|
||
|
||
### Recovering local process state
|
||
|
||
On a named instance, `mosaic org lock show ID` reports the lifecycle lock's
|
||
PID, recorded start timestamp and whether that PID is alive. `mosaic org lock
|
||
clear ID` removes it only when the PID is provably dead. A recycled live PID,
|
||
malformed lock, or uncertain process state requires inspection. Registry locks
|
||
have no holder information and cannot be recovered by this command.
|
||
|
||
`mosaic bus recover ID` refuses a live matching host, live writer, listening
|
||
socket, mismatched lock or uncertain state. It preserves the old host and lock
|
||
information in a `bus-host/recovery-*.json` receipt before removing the stale
|
||
state, dead writer lock and inactive socket. It never sends a signal. Retry
|
||
`mosaic bus start ID` after recovery.
|
||
|
||
`mosaic console start` and `mosaic console stop` move a dead or mismatched
|
||
Console state to `console.json.stale-<UTC timestamp>-<suffix>`, report the
|
||
move on stderr, and continue. A live matching Console blocks another start.
|
||
`mosaic console status` prints the recorded state (pid, URL, `bind`, and
|
||
`allowHosts` for a LAN Console) and whether that process still runs. It moves
|
||
nothing. `--dev-insecure-bind` and `--dev-allow-host` are in docs/TOOLS.md.
|