Rebased on row 59 (a8512007). Darkwing's N1-N5 and the Q1 proxy line (lead decision 99), Filbert's N1-N4 (lead decision 100): a 408 is unknown on the send box and the board Reply, the receipt shows the code once, focus and caret kept, a role no longer held kept, Inbox current on a message trail, README notes. 35 of 35 mutants killed; gate green on729da72b. Manifest sha256 04ecf1b57c594a35d34d53277b2df915a571f4fa9f7cc29146dbedfa612f77d4. Co-Authored-By: Claude Opus 5.5 <[email protected]>
520 lines
30 KiB
Markdown
520 lines
30 KiB
Markdown
# Row 76 (#1570): Console sends a human message to a role, round 2
|
|
|
|
Owner: Dewey. Reviewers: Darkwing and Filbert. Brief:
|
|
`docs/plans/2026-10-11_dogfood-cutover.md` § "Console sends a message to a
|
|
role", blob `f5d56554` (rev 552). Jason's Q95 answer is lead decision 95;
|
|
the reply view question is lead decision 97, option (a). Base `729da72b`
|
|
(row 59 landed in `a8512007`). None of the candidate's paths changed
|
|
between `729da72b` and `refactor` HEAD (`729da72b`) when the packet was
|
|
built.
|
|
|
|
Round 2 is the rebase on row 59, Darkwing's round 1 notes N1 to N5 with
|
|
a Q1 line in the README (lead decision 99), and Filbert's round 1 notes
|
|
(lead decision 100). They are listed separately below: "Round 2: rebase
|
|
on row 59" for the merged hunks, then "Round 2: Darkwing's notes (lead
|
|
decision 99)" and "Round 2: Filbert's notes (lead decision 100)" for the
|
|
content changes.
|
|
The rest of this file is round 1's, updated where those changes touch it,
|
|
with the mutants, gate and demonstration rerun on the round 2 tree.
|
|
|
|
`build.patch` is `git diff --binary 729da72b -- packages/webui`, the two
|
|
new test files included. Apply it with `git apply --index` to a detached
|
|
worktree of the base; `candidate-manifest.sha256` then checks 9/9 OK.
|
|
|
|
## What ships
|
|
|
|
`serve --public-host NAME --auth-header HEADER --allow-user ID --send`.
|
|
`--send` is off by default; without it the Console behaves as after row 74
|
|
(`preview.test.mjs` and `serve.test.mjs` unchanged and green).
|
|
|
|
**Start (`cli.mjs`, `serve.mjs`).** `--send` is the one flag without a
|
|
value. Without all three preview flags the CLI exits 2 with `refused:
|
|
--send needs the preview mode (...)`, and `startServer` throws the same
|
|
for a direct caller, or for a `send` that isn't a function. With `--send`
|
|
and no system config the start refuses (`--send needs the bus configured
|
|
(...)`, the reason redacted as before) instead of serving a box that can't
|
|
send; a read-only Console still serves without the config. The `Preview:`
|
|
line says reads and human messages to roles, one line per send below.
|
|
`busReader` is split into `busCaller` (the human transport call, the
|
|
business resolved per request as before) and `views` over it, so the
|
|
reads and the send use the same transport.
|
|
|
|
**The route.** With `--send`, `POST /api/bus/message` is the one write
|
|
preview mode lets past its read-only check; any other method on that path
|
|
is 405, and every other write is still 403 `read-only`. Order, each on
|
|
every request:
|
|
|
|
1. The shared checks from row 74 run first: repeated Host 400, HEADER with
|
|
a Host other than NAME 403 `proxy Host refused`, a NAME request without
|
|
HEADER = ID once 401, a sent Origin other than `https://NAME` 403. These
|
|
write no log line.
|
|
2. `busSend` checks HEADER = ID once again itself (401), so the route
|
|
never runs on a loopback Host: a loopback request without HEADER is
|
|
401, with it 403 `proxy Host refused`. There is no loopback send.
|
|
3. Origin sent once and exactly `https://NAME` (a missing Origin passes
|
|
the shared check and stops here), else 403 `cross-origin request
|
|
refused`.
|
|
4. `Sec-Fetch-Site` sent once and exactly `same-origin`, else 403
|
|
`cross-site request refused`.
|
|
5. `Content-Type: application/json`, at most 16384 bytes, a JSON object
|
|
with `to` and `body`, optionally `in_reply_to`, no other key; else 400
|
|
`invalid-request`.
|
|
6. `message.send` with exactly those keys. The broker judges `to` and the
|
|
body. Its limit, 32768 characters, is past the request limit, so 16 KiB
|
|
binds first.
|
|
|
|
Every answer after step 2 carries `sent`: `false` for a refusal, `"unknown"`
|
|
when the message may exist. Success is 200 `{id, at}`.
|
|
|
|
**Outcomes (`bus.mjs` `sendOutcome`).** The broker writes in one
|
|
transaction, so any code it returns means nothing was written:
|
|
`{error: code, sent: false}` at the read routes' statuses (`busStatus`),
|
|
with `unknown-role`, `message-not-found` and `request-too-large` added as
|
|
400. `outcome-unknown`, `invalid-response` and `response-too-large` from
|
|
the transport, any error that isn't a `BusReadError`, and an answer
|
|
without a valid message id leave it open: 503 `outcome-unknown` with
|
|
`sent: "unknown"`. Nothing retries.
|
|
|
|
**Log.** One stdout line per request that gets past step 2: the time,
|
|
`send`, `user=` (JSON-quoted), `to=` (JSON-quoted, cut to 64 characters, or
|
|
`none`), then `id=<message id>` or `error=<code>`. Never the body. A test
|
|
shows a `to` with a newline can't forge a second line.
|
|
|
|
**`/api/config`** gains `send`: true only with `--send` on a NAME request,
|
|
so the operator's loopback page never shows a box that would get 401.
|
|
|
|
**The page (`public/bus.js`).** With `send` true the Inbox has "Message a
|
|
role": a picker of held roles from the agents read, a text box and Send.
|
|
|
|
- The draft (role and text) lives in page memory, so the ten-second
|
|
refresh redraws the form without losing it; focus and the caret are
|
|
kept. A reload loses it. A role no longer held stays selected, marked
|
|
"(not held now)".
|
|
- Send is disabled while a send is pending; a second press sends nothing.
|
|
An empty role or text is refused in the page without a request.
|
|
- One POST per Send, `redirect: 'manual'`. Sent: an id came back; the
|
|
receipt shows it and links `#/trail/message/<id>` (a new trail kind: the
|
|
message and its deliveries, back link to the Inbox, not-found for an id
|
|
the trail doesn't hold). The text is cleared unless it was edited during
|
|
the send.
|
|
- Not sent: a 4xx other than 408, or the server's `sent: false` (a 503
|
|
`no-bus-host` too), or a redirect. Only the sign-in proxy redirects, when its session
|
|
has expired, so the request never reached the Console; the receipt asks
|
|
for a reload. The draft is kept.
|
|
- Anything else, a lost answer and a 408 included: outcome unknown. The receipt says
|
|
the message may have been sent, that nothing was sent again, and that
|
|
there is no trail to link without an id; the draft is kept.
|
|
- The box says a reply comes back as a message the Console doesn't show
|
|
yet, and the next `mosaic talk` prints it after sending its own message
|
|
(decision 97).
|
|
|
|
**Reply in the board (`public/app.js`).** A refused Reply said "Delivery
|
|
may be unknown; check the seat before sending again" for a 403. A 4xx other
|
|
than 408 now says "Not sent."; no answer, an unreadable one, a 5xx or a
|
|
408 keeps the old text. Row 74 left this for this row.
|
|
|
|
**README.** A "Message a role" section: the command, what `--send` grants
|
|
(anyone the proxy lets through as ID, and any local process that reaches
|
|
the port and sets HEADER with `Host: NAME`; decisions 62 and 95), the
|
|
request and each check, the broker's refusals and their statuses, the
|
|
unknown outcome, the log line, the page's three receipts, and the reply
|
|
path with `mosaic talk`'s limits (decision 97). The routes list gains the
|
|
message trail; the "nothing writes" lines name the exception. Folded in
|
|
from row 74 per the row note: Filbert N1 ("its first signed-in request"),
|
|
Darkwing's tunnel sentence (anything reaching a tunnel's local end can
|
|
send a loopback Host without HEADER, so bind it to loopback), and a
|
|
pointer to `scripts/webui-service.sh`, which takes no `--send`.
|
|
|
|
## Decisions and tradeoffs
|
|
|
|
1. **No loopback send.** The brief asks for the header on every request,
|
|
loopback included. A loopback request can't carry it (row 74 refuses
|
|
HEADER with a loopback Host), so the operator's browser on `127.0.0.1`
|
|
reads but can't send. `/api/config` hides the box there.
|
|
2. **The route checks the header itself** even though the shared check
|
|
already did for NAME requests. It costs one line and keeps the route
|
|
closed if the shared check changes.
|
|
3. **A missing Origin is refused on the send** (the shared check allows a
|
|
missing one for reads). The brief asks for the request's own origin,
|
|
and browsers send Origin on a POST.
|
|
4. **`Sec-Fetch-Site` is compared exactly**, so `Same-Origin` and a
|
|
repeated header are refused. Browsers send the lowercase value once.
|
|
5. **Which errors mean "not sent".** The brief maps broker codes like the
|
|
reads'. I treat every coded broker answer as definite, since the broker
|
|
writes in one transaction, and only the transport's three lost-answer
|
|
codes, an uncoded error or a missing id as unknown. The page adds 4xx
|
|
and the redirect as definite. A proxy 4xx means the request wasn't
|
|
accepted for forwarding; a proxy 5xx (502, 504) can come after the
|
|
Console got it, so it stays unknown, and so does a 408 (round 2,
|
|
Filbert's N1).
|
|
6. **The draft is in page memory, not storage.** The brief asks for it to
|
|
survive the periodic refresh. Keeping it out of `localStorage` keeps a
|
|
message body off disk on a shared browser profile.
|
|
7. **`--send` without the bus refuses to start.** The brief doesn't say.
|
|
Serving a box that can only answer `not-configured` seemed worse than
|
|
a refusal at start.
|
|
|
|
## Where the brief and the code differ (for Sage)
|
|
|
|
- "Replies show in the inbox as today": they don't; the inbox shows
|
|
decisions. Decision 97 settled it: the README and the box give `mosaic
|
|
talk`'s limits, and the reply view is row 81 (#1575).
|
|
- "Points at the trail" on an unknown outcome: there's no id to link, so
|
|
the receipt points at the role's side and the bus trail from a terminal.
|
|
- The log's `error=` value is the code as answered, unquoted; for the 403s
|
|
it has spaces (`error=cross-origin request refused`). It is always last
|
|
on the line.
|
|
|
|
## Round 2: rebase on row 59
|
|
|
|
Round 1's candidate was on `3d298920` (candidate manifest `ca870314`,
|
|
request comment 27594). Row 59 landed first in `a8512007`, so I applied
|
|
round 1's `build.patch` to `729da72b` with `git apply --3way --index`.
|
|
Every file applied cleanly, with no conflict.
|
|
|
|
Compared by added and removed lines, file by file, round 2's patch is
|
|
identical to round 1's in all nine files. Only the context around the
|
|
hunks moved. Two manifest digests change, `packages/webui/README.md` and
|
|
`packages/webui/src/public/bus.js`, because row 59 changed those files
|
|
too. The other seven digests are the same as round 1's.
|
|
|
|
Row 59's hunks in this row's files, and how each merged:
|
|
|
|
- `packages/webui/README.md`: row 59 adds the "Agent state (row 59,
|
|
#1548)" section, lines 270-280 in the candidate, between "Inbox, tasks,
|
|
agents and trails" and "Shell". My nearest hunks are in "Inbox, tasks,
|
|
agents and trails" above it (the route list gains
|
|
`#/trail/message/<id>`, and "Nothing writes" names the send box) and in
|
|
"Data and boundaries" below it (without `--send` no route writes).
|
|
Neither touches row 59's section. It merged cleanly.
|
|
- `packages/webui/src/public/bus.js`: row 59 adds `stateMark` and
|
|
`attention` and changes `agentsView`, lines 367-390 in the candidate
|
|
(State and Attention columns). My hunks sit above it (the header
|
|
comment, and the send form's `draft` and `composerHtml`, lines 246-287)
|
|
and below it (the message trail, the router, focus restore, and the
|
|
submit handler). Neither touches the agents view. It merged cleanly.
|
|
`stateMark`, `attention`, `plural`, `blockChip`, `first` and `short`
|
|
are each defined once.
|
|
|
|
Row 59's other files (`packages/bus/README.md`, `broker.mjs`,
|
|
`agents.test.mjs`, the webui `bus.css`, `bus-browser.test.mjs` and
|
|
`bus.test.mjs`) aren't files this row touches. The mutants, the suites and the
|
|
demonstration below ran on the round 2 tree.
|
|
|
|
## Round 2: Darkwing's notes (lead decision 99)
|
|
|
|
Content changes made after the rebase, against Darkwing's round 1 review
|
|
(comment 27612, `agents/darkwing/work/queue-76-review/review-r1.md`). They
|
|
change three files: `packages/webui/README.md`, `src/public/bus.js` and
|
|
`tests/send-browser.test.mjs`. Each was a small change; none is left for a
|
|
follow-up row.
|
|
|
|
- **N1, the board Reply's "Not sent." (`app.js:378`) held by a browser
|
|
test.** `send-browser.test.mjs`'s stub now answers `/api/reply` from a
|
|
queue when one is set. The test opens an inspection, types a reply, and
|
|
answers a 403 and then a 502. The 403 must show `failed: refused here.
|
|
Not sent.` and the 502 `failed: board unreachable. Delivery may be
|
|
unknown; check the seat before sending again.`, the reply text kept both
|
|
times, and the board's capture list stays empty. `app.js` itself is
|
|
unchanged from round 1. The mutants `reply-4xx-unknown` and
|
|
`reply-5xx-refused` check that the test catches both directions.
|
|
- **N2, the caret across a refresh.** The refresh case now selects
|
|
characters 3 to 7 in the text box, presses Refresh, and asserts focus
|
|
and the selection `{start: 3, end: 7}`, after the pattern at
|
|
`browser.test.mjs:55`. Mutants `page-no-caret` (not restored) and
|
|
`page-caret-not-kept` (not saved).
|
|
- **N3, README "Message a role".** "It is recorded exactly like one sent
|
|
from the CLI" is now "It goes the same way as one sent from the CLI,
|
|
with class INFO where `mosaic talk` uses REQUEST". The class stays INFO.
|
|
The box's note in `bus.js` said the same thing ("recorded by the bus
|
|
exactly like one sent from the CLI"); it now says "the same way as one
|
|
sent from the CLI, with class INFO". No test pins that sentence.
|
|
- **N4, README reply path.** "talk prints each one and records it
|
|
delivered" is now: 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 (`message.read`,
|
|
`cli.mjs:208`).
|
|
- **N5, the code shown twice in a refusal receipt.** The broker's
|
|
refusals arrive as `not sent: the bus refused it (unknown-role)`, and
|
|
the receipt added ` (unknown-role)` again, after its own "Not sent to
|
|
coder:". The receipt now drops the server's leading `not sent: ` and
|
|
adds the code only when the message doesn't already carry `(code)`. It
|
|
reads `Not sent to coder: the bus refused it (unknown-role). The draft is
|
|
kept.` A refusal whose message lacks the code (a 4xx from a check before
|
|
the bus, or a proxy's) still gets it. Dropping the prefix goes past N5's
|
|
letter; without it the receipt said "Not sent to coder: not sent: ...".
|
|
The browser test now asserts the two receipts as exact strings (the 400
|
|
`unknown-role` and the 503 `no-bus-host`). Mutants `page-code-twice` and
|
|
`page-keeps-not-sent-prefix`.
|
|
- **Q1, the proxy contract.** A new bullet in "What the proxy must do":
|
|
the proxy 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), because the page reads a 4xx as "not sent"
|
|
and a rewritten 503 `outcome-unknown` would then report a message that
|
|
may exist as not sent.
|
|
- **Q2** stays as built (Darkwing: yes, refuse at start).
|
|
|
|
## Round 2: Filbert's notes (lead decision 100)
|
|
|
|
Filbert's round 1 review (comment 27637, approve). These change
|
|
`packages/webui/README.md`, `src/public/bus.js`, `src/public/app.js`,
|
|
`tests/send.test.mjs` and `tests/send-browser.test.mjs`. N1 is the one
|
|
behaviour change; N3 is tests only. Each was a small change; none is left
|
|
for a follow-up row. Filbert's N2 is Darkwing's N1 and their N4 is
|
|
Darkwing's N5, both above.
|
|
|
|
- **N1, a 408 is unknown, not "not sent".** A proxy can answer 408 after
|
|
it has forwarded the request (Envoy's `stream_idle_timeout`, when no
|
|
upstream headers arrived in time). The send box's check
|
|
(`bus.js`, the `refused` branch) and the board Reply's (`app.js:378`) now
|
|
both leave 408 out of the 4xx they read as not sent; a 408 shows the
|
|
unknown receipt. A 408 that carries the Console's own `sent: false` would
|
|
still read as not sent, but the Console never answers 408. Tests: the
|
|
send box's unknown loop gains a 408, and the board Reply case a third
|
|
answer, 408, which must say delivery may be unknown with the text kept.
|
|
README: the "Outcome unknown" bullet says a 408 counts as unknown, here
|
|
and on the Reply, and the Q1 proxy bullet says "a 4xx other than 408".
|
|
Mutants `page-408-refused` and `reply-408-refused`.
|
|
- **N3, Filbert's surviving mutants, each now held by a test:**
|
|
- `p-no-field-focus` and `p-no-caret`: the refresh case leaves focus in
|
|
the text box (the button is clicked from script, which doesn't move
|
|
focus), then asserts focus and the selection (Darkwing's N2 above).
|
|
Mine: `page-no-field-focus`, `page-no-caret`, `page-caret-not-kept`.
|
|
- `p-not-held-dropped`: with `pm` selected, the test ends `pm`'s run in
|
|
the broker (`endLaunch`, which releases its claim) and refreshes. The
|
|
picker must still hold `pm`, selected, as `pm (not held now)`. Mine:
|
|
`page-not-held-dropped`.
|
|
- `p-inbox-not-current`: on `#/trail/message/<id>` the Inbox is the
|
|
current section (`aria-current="page"`). Mine:
|
|
`page-inbox-not-current`.
|
|
- `s-reply-truthy`: the passthrough test sends `in_reply_to: ""` and
|
|
asserts the key reaches `send` with the empty value. Mine:
|
|
`send-reply-truthy`.
|
|
- `s-plain-error-code`: the plain `Error` in the outcomes table
|
|
(`send.test.mjs`, the `ENOENT` case) now carries `code: 'ENOENT'`, as a
|
|
socket error does, and must still be 503 `outcome-unknown`. Mine:
|
|
`outcome-plain-error-code`.
|
|
- `s-log-cut-65`: the forged-line test asserts the logged `to` is the
|
|
first 64 characters exactly, and a 65-character role logs without its
|
|
last one. Mine: `send-log-cut-65`.
|
|
- `s-array-ok` is equivalent, as Filbert says; no test.
|
|
|
|
## Rebase on row 60 (round 1)
|
|
|
|
I built this row on `bbd22f1b`. Row 60 landed first (`1771bf98`), so I
|
|
applied my patch to `3d298920` with `git apply --3way`. Row 60's hunks in
|
|
this row's files, and how each merged:
|
|
|
|
- `packages/webui/src/serve.mjs`: row 60's `BUS_VERBS` line (adds
|
|
`credentials`). It merged cleanly; my changes there are elsewhere in the
|
|
file.
|
|
- `packages/webui/README.md`: row 60's credentials read and its line in
|
|
the exposure list. Merged cleanly, in different paragraphs from my
|
|
"Message a role" section and the "nothing writes" lines.
|
|
- `packages/webui/src/bus.mjs`: the header comment, the one conflict. Row
|
|
60 named its `credentials` read and I named the one write. The merged
|
|
comment says both:
|
|
|
|
```
|
|
// Only the four verbs of the Q1 module (lead decision 56), row 57's
|
|
// `decisions` and row 60's `credentials` reads are reachable as reads: no
|
|
// read writes, not even seen (Q4). The one write is row 76's human
|
|
// `message.send` (lead decision 95), built by the CLI only with --send. The
|
|
// bus does the proof; a server started inside an agent run gets
|
|
// `human-required` from the broker.
|
|
```
|
|
|
|
Row 60's `tests/bus.test.mjs` change isn't in a file this row touches. The
|
|
mutants, the suites and the demonstration below ran on round 2's tree (the
|
|
rebase on `729da72b` with every round 2 change).
|
|
|
|
## Files
|
|
|
|
| File | Change |
|
|
|---|---|
|
|
| `packages/webui/src/serve.mjs` | `busSend`, the route under `send`, `body()` limit parameter, `send`/`log` options, `/api/config` `send` |
|
|
| `packages/webui/src/bus.mjs` | `busCaller` split out of `busReader`; `sendOutcome`; three codes added to the status map |
|
|
| `packages/webui/src/cli.mjs` | `--send`, refused without the preview flags or the bus; the `Preview:` line |
|
|
| `packages/webui/src/public/bus.js` | the "Message a role" box, the message trail kind, focus and caret kept across a refresh; round 2: the refusal receipt shows the code once, the box's note, a 408 stays unknown |
|
|
| `packages/webui/src/public/app.js` | the Reply 4xx text; round 2: a 408 stays unknown |
|
|
| `packages/webui/README.md` | the "Message a role" section and the folded-in row 74 notes; round 2: Darkwing's N3 and N4, the Q1 proxy bullet, the 408 line |
|
|
| `packages/webui/tests/send.test.mjs` | new, 13 tests; round 2: `in_reply_to: ""`, a coded plain error, the 64-character log cut |
|
|
| `packages/webui/tests/send-browser.test.mjs` | new, 1 browser test; round 2: the board Reply 403, 502 and 408, focus and caret, a role no longer held, exact refusal receipts, a 408 send, Inbox current on a message trail |
|
|
| `packages/webui/tests/shell.test.mjs` | the pinned write boundary: bus.js has exactly one write, this one |
|
|
|
|
## Tests
|
|
|
|
`send.test.mjs` (13): `startServer` refuses `send` without the preview
|
|
mode or as a non-function; the CLI refuses `--send` without the flags and
|
|
without a configured bus (exit 2, nothing on stdout, the config path not
|
|
named); the CLI with `--send` and a configured bus but no bus host running
|
|
prints the `Preview:` line, answers a send 503 `no-bus-host` with `sent:
|
|
false` through the real human transport, and logs one line without the
|
|
body; without `--send` the route is 403 `read-only` and
|
|
config says no send; with `--send` every other write (POST, PUT, DELETE,
|
|
PATCH on the board, bus and page routes) is still 403 `read-only` and
|
|
nothing reaches the board or bus; config says send only on NAME; 401 for no
|
|
header, another user, a suffix, two values, loopback without the header
|
|
and `localhost` with no Origin, and 403 `proxy Host refused` for loopback
|
|
with it, none sent or logged; each request check (no Origin, no or wrong
|
|
or repeated `Sec-Fetch-Site`, no or wrong Content-Type, a form, 16385
|
|
bytes, extra keys, `business`, missing `to` or `body`, not JSON, null, an
|
|
array, a string) refused with `sent: false`, logged without the body, and
|
|
16384 bytes accepted; wrong Origins refused by the shared check; GET and
|
|
PUT 405; exactly `to`, `body` and `in_reply_to` passed on, an empty
|
|
`in_reply_to` included; a crafted `to` can't forge a log line, and `to` is
|
|
cut at 64 characters; each bus refusal with its status and `sent:
|
|
false`, each open outcome 503 `unknown` (a plain error with a code too), a missing id unknown, and one
|
|
call each (no retry); the human transport request is exactly the business,
|
|
`message.send` and the keys; against a real broker the message is written
|
|
once as a human send (`from_role` human, `from_run` human-cli, class INFO)
|
|
and `unknown-role`, `invalid-request` and `message-not-found` map to 400.
|
|
|
|
`send-browser.test.mjs` drives the page in Chrome with an in-process
|
|
broker for the roles and the trail and a stub for the POST (a send passes
|
|
only with the proxy's https Origin, which a loopback page can't have): no
|
|
box without `send`; the held roles; empty role and text refused without a
|
|
request; the draft, focus and caret across a refresh; a role no longer
|
|
held kept selected as "(not held now)"; a 400 and a 503
|
|
`sent: false` refused with the draft kept, each receipt asserted exactly
|
|
with its code once; a redirect refused as `sign-in`; an unknown
|
|
outcome, a network failure, a 200 without an id and a 408 each unknown, with no
|
|
trail link and no resend; one POST per Send, pinned as
|
|
`{method, headers, body, redirect: 'manual'}`; Send disabled while
|
|
pending and a second press ignored; text typed during a send kept; a plain
|
|
success clears the text and keeps the role; the receipt's trail link opens
|
|
the message's trail, with the Inbox the current section; an unknown
|
|
message id is not found; the board's Reply answered 403 says "Not sent."
|
|
and 502 and 408 say delivery may be unknown, the reply text kept each time
|
|
and nothing captured.
|
|
|
|
## Mutants
|
|
|
|
`mutants.py` makes 35 single-site edits, one at a time, in a separate
|
|
worktree of round 2's tree (never the build tree). Each mutant runs against
|
|
the whole webui suite, and the file is restored from the copy read before
|
|
the edit. The baseline passed, the worktree was unchanged afterwards, and
|
|
**35 of 35 were killed** (2026-10-12T01:54:19Z to 02:29:20Z). `mutants.txt`
|
|
lists each mutant with the tests that killed it.
|
|
|
|
Round 1's 21:
|
|
|
|
- Route (`serve.mjs`): the route's own header check removed; Origin
|
|
unchecked; `Sec-Fetch-Site` unchecked, or `same-site` let through; the
|
|
limit raised to 64 KiB; extra keys allowed; `in_reply_to` dropped; the
|
|
id check removed; the log's `to` unquoted and uncut; `/api/config`
|
|
`send` true on loopback; the route open without `--send`; every write
|
|
opened by `--send` (`send-opens-writes`, killed by the "every other
|
|
write is still 403" test).
|
|
- Outcomes (`bus.mjs`): only `outcome-unknown` left open; `unknown-role`
|
|
dropped from the status map.
|
|
- Page (`bus.js`): no pending guard; the text cleared though edited during
|
|
the send; a 5xx `sent: false` treated as unknown; an unknown outcome
|
|
resent; a redirect treated as unknown; redirects followed; the message
|
|
trail shown for any id.
|
|
|
|
Round 2's 14, one or more for each reviewer note that changed behaviour or
|
|
asked for a test:
|
|
|
|
- Board Reply (`app.js`): a 4xx read as unknown (`reply-4xx-unknown`); a
|
|
5xx read as not sent (`reply-5xx-refused`); a 408 read as not sent
|
|
(`reply-408-refused`). Darkwing's N1, Filbert's N2 and N1.
|
|
- Send box (`bus.js`): the caret not restored (`page-no-caret`), restored
|
|
to the end (`page-caret-not-kept`), focus not restored
|
|
(`page-no-field-focus`); the code shown twice (`page-code-twice`); the
|
|
"not sent: " prefix kept (`page-keeps-not-sent-prefix`); a 408 read as
|
|
not sent (`page-408-refused`); a role no longer held dropped from the
|
|
picker (`page-not-held-dropped`); the Inbox not current on a message
|
|
trail (`page-inbox-not-current`). Darkwing's N2 and N5, Filbert's N1 and
|
|
N3.
|
|
- Route and outcomes: an empty `in_reply_to` dropped (`send-reply-truthy`);
|
|
a plain error with a `code` read as definite
|
|
(`outcome-plain-error-code`); the log's `to` cut at 65
|
|
(`send-log-cut-65`). Filbert's N3.
|
|
|
|
Round 1's run on the `bbd22f1b` build and its 21-of-21 run on the row 60
|
|
rebase are not repeated here; this run covers all 35 on the final tree.
|
|
|
|
## Demonstration
|
|
|
|
`demo.mjs` runs the candidate's `startServer` with `--send`'s options
|
|
against a real in-process broker (pm, cto and coder; coder claimed),
|
|
driven over `node:http` on loopback with the headers the proxy and the
|
|
browser send (`Host: NAME`, `x-authentik-uid: jason`, Origin,
|
|
`Sec-Fetch-Site`). Three servers: `send`, `lost` (the broker writes, then
|
|
the call throws `outcome-unknown`, as when the transport loses the
|
|
answer) and `readonly` (no `--send`). `demo.txt` is its output on the gate
|
|
worktree of round 2's tree:
|
|
|
|
```text
|
|
== a send, and a reply to the coder
|
|
to coder 200 {"id":"9db1d4eb-2abd-4c13-a388-2ed3d9503c07","at":"2026-10-12T02:37:11.862Z"}
|
|
to coder, in_reply_to the coder's message 200 {"id":"c4b38d2a-34df-4dea-b31b-897f7de01777","at":"2026-10-12T02:37:11.877Z"}
|
|
== refused before the bus
|
|
no sign-in header (401, no log line) 401 {"error":"sign-in required"}
|
|
loopback Host with the header 403 {"error":"proxy Host refused","message":"the proxy must forward Host unchanged"}
|
|
another Origin (shared check, no log line) 403 {"error":"cross-origin request refused"}
|
|
no Origin 403 {"error":"cross-origin request refused","message":"a send needs the Console's own Origin","sent":false}
|
|
Sec-Fetch-Site: same-site 403 {"error":"cross-site request refused","message":"a send needs Sec-Fetch-Site: same-origin","sent":false}
|
|
Content-Type: text/plain 400 {"error":"invalid-request","message":"Content-Type must be application/json","sent":false}
|
|
16385 bytes 400 {"error":"invalid-request","message":"body larger than 16384 bytes","sent":false}
|
|
an extra key 400 {"error":"invalid-request","message":"a send takes to, body and an optional in_reply_to, nothing else","sent":false}
|
|
GET 405 {"error":"method not allowed"}
|
|
== refused by the broker
|
|
a role the business lacks 400 {"error":"unknown-role","message":"not sent: the bus refused it (unknown-role)","sent":false}
|
|
an empty body 400 {"error":"invalid-request","message":"not sent: the bus refused it (invalid-request)","sent":false}
|
|
in_reply_to a missing message 400 {"error":"message-not-found","message":"not sent: the bus refused it (message-not-found)","sent":false}
|
|
== the answer lost after the broker wrote
|
|
unknown, not retried 503 {"error":"outcome-unknown","message":"the message may have been sent; nothing was retried. Check the trail before sending again","sent":"unknown"}
|
|
== without --send
|
|
POST is read-only 403 {"error":"read-only","message":"the Console preview is read-only"}
|
|
== what the broker holds
|
|
9db1d4eb from=human/human-cli to=coder class=INFO in_reply_to=- body="Please look at row 76 before the cutover."
|
|
c4b38d2a from=human/human-cli to=coder class=INFO in_reply_to=8c236a3d-c415-4252-818c-015cdd81d185 body="refactor"
|
|
pm receives 1 message(s) from the lost-answer send: ["Lost answer test"]
|
|
== the Console log, one line per send past sign-in
|
|
2026-10-12T02:37:11.862Z send user="jason" to="coder" id=9db1d4eb-2abd-4c13-a388-2ed3d9503c07
|
|
2026-10-12T02:37:11.877Z send user="jason" to="coder" id=c4b38d2a-34df-4dea-b31b-897f7de01777
|
|
2026-10-12T02:37:11.881Z send user="jason" to=none error=cross-origin request refused
|
|
2026-10-12T02:37:11.882Z send user="jason" to=none error=cross-site request refused
|
|
2026-10-12T02:37:11.883Z send user="jason" to=none error=invalid-request
|
|
2026-10-12T02:37:11.885Z send user="jason" to=none error=invalid-request
|
|
2026-10-12T02:37:11.886Z send user="jason" to=none error=invalid-request
|
|
2026-10-12T02:37:11.892Z send user="jason" to="auditor" error=unknown-role
|
|
2026-10-12T02:37:11.899Z send user="jason" to="coder" error=invalid-request
|
|
2026-10-12T02:37:11.905Z send user="jason" to="coder" error=message-not-found
|
|
2026-10-12T02:37:11.913Z send user="jason" to="pm" error=outcome-unknown
|
|
a body in the log: false
|
|
```
|
|
|
|
## Gate (my run on the candidate)
|
|
|
|
Detached worktree of `729da72b`, `build.patch` applied with `git apply
|
|
--index`, the manifest (sha256 `04ecf1b5…`) 9/9 OK before and after.
|
|
Suites one after another, `TMPDIR` in my scratch, Docker on,
|
|
2026-10-12T02:29:20Z to 02:37:11Z:
|
|
|
|
| Suite | Result |
|
|
|---|---|
|
|
| `packages/webui` | 76 pass, 0 fail, 0 skipped, the browser tests included (14 new) |
|
|
| `packages/bus` | 94 pass, 0 fail (88 in round 1; row 59 added 6) |
|
|
| `packages/cli` | 114 pass, 0 fail |
|
|
| `packages/control-board` | 124 pass, 0 fail |
|
|
| business, conversation, discord, harness, ledger, mosaic, queue, runs, seat, tasks | all 0 fail (60, 182, 178, 59, 78, 69, 150, 41, 27, 71 pass) |
|
|
| build-tokens `--check` | exit 0 |
|
|
| test-auth, test-conductor, test-config, test-discord, test-extension-package, test-foundation | exit 0 (15, 17, 24, 66, 18, 44 passed) |
|
|
| test-queue | 27 passed, 0 failed; its `verify`/`render --check` case skips outside the canonical checkout |
|
|
| test-release | 14 passed, 0 failed (Docker, nothing skipped) |
|
|
| test-task | 98 passed, 0 failed (Docker, nothing skipped) |
|
|
|
|
`git status` afterwards shows only the patch's 9 staged files and the
|
|
`node_modules` link. `core.hooksPath` stayed unset.
|
|
|
|
## Questions for reviewers
|
|
|
|
Round 1's two questions are answered. Q1 (decision 5, a proxy 4xx shown
|
|
as "not sent"): Darkwing said yes, with a proxy-contract line, now added;
|
|
Filbert said yes for nginx, Caddy and Traefik except 408, now unknown.
|
|
Q2 (decision 7, refusing `--send` at start without the bus config): both
|
|
said yes; kept as built. Round 2 has no new question.
|