Files
boxmaker/docs/plans/M4a/README.md
T
kyleandClaude Opus 5.5 6feffadd11 M4a task 16: read a secret file from the file that was checked (review finding 2)
The test swaps the file between the check and the open through a `between` hook. A reference fix
passed it and the gate (769 tests) in the working tree, caught two mutations, and was removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 01:27:41 -07:00

115 lines
8.6 KiB
Markdown

# M4a implementation plan: `gatewayd`, conversations over Mattermost
> **For the implementing model:** do not work from this file. The owner gives you one task file at
> a time (`01-…` to `15-…`). This file is the index for the owner and the reviewer.
**Goal:** a direct message to Boxmaker's bot account on Mattermost becomes a turn in `loopd`, and
the answer is posted in its thread. In channels shared with other agents, Boxmaker answers only
posts that name it, and replies in its own threads that name nobody else. Anyone not on the
allowlist gets nothing at all. `gatewayd` opens no listening port.
**Architecture:** `proto` gains SHA-1 (01). `gatewayd` gets its dependencies and test certificates
(02), its configuration (03), its secrets and the runbook entries (04), then a network layer built
bottom-up: TCP or TLS (05), HTTP/1.1 (06), the WebSocket handshake (07), frames (08) and the
connection (09). Mattermost's JSON and REST calls (10), the routing of posts to sessions (11), the
state file (12) and delivery on `loop.sock` (13) are each one module. The serve loop ties them
together (14), and `main` starts it (15).
**Spec:** `docs/specs/2026-09-23-m4a-gateway.md`. Brief: `docs/design.md` (P15 applied). Every
fail-closed message ends with a pointer into `docs/runbook.md`; task 04 adds all seven entries
this milestone needs.
**No task needs Mattermost or a network.** Everything runs against fakes: TLS test servers with a
test-only CA, a scripted WebSocket server, a scripted HTTP server, a fake Mattermost and a fake
`loopd`. The checks against the owner's server are done by the design model afterwards.
## Global constraints
- Everything in `AGENTS.md`, including "Lessons from earlier reviews".
- New dependencies, all in task 02 and no others: `rustls` 0.23.45 (no default features; `ring`,
`std`, `tls12`), `rustls-native-certs` 0.8.4, `zeroize` 1.9.0; `gatewayd` also uses `serde`,
`serde_json` and `toml`, already vetted.
- Branch `m4a`. One task, one fresh OpenCode session, one commit. Run `cargo fmt --all` before the
gate. Review happens once, after task 15.
- Most tasks hand over a **skeleton**: the file with its types, constants and signatures written,
and `todo!()` bodies with the steps as comments above them. Fill one function at a time and run
`cargo check -p <crate>` after each (tip T25). Keep the comments; they say why.
- Mattermost's JSON is another program's format: unknown fields are ignored. Our formats
(`gatewayd.toml`, `state.json`) reject them.
## Tasks
The last column is how the given tests were checked before hand-over (tip T17). Every task had a
reference implementation, and the given tests were run against it at that task's end state. Then
each task's end state was rebuilt on its own from master in this order, with the gate at every
step (tip T26), and the reference was deleted so it cannot be read (tip T18). Each skeleton was
checked to compile against its task's tests and fail them.
| # | File | Delivers | Tests | Check |
|---|---|---|---|---|
| 01 | `01-proto-sha1.md` | `proto::sha1` | `proto/tests/sha1.rs` | reference; vectors checked with `sha1sum` |
| 02 | `02-gatewayd-deps.md` | dependencies, `deny.toml`, TLS test certificates | none new | reference |
| 03 | `03-gatewayd-config.md` | `gatewayd.toml` | `config.rs`, `support/tmp.rs` | reference |
| 04 | `04-gatewayd-secrets.md` | `SecretStore`: credential, env, file; seven runbook entries | `secrets.rs` | reference |
| 05 | `05-gatewayd-net.md` | TCP or TLS, verified | `net.rs`, `support/tls_server.rs` | reference |
| 06 | `06-gatewayd-http.md` | HTTP/1.1 requests, size caps, rate-limit waits | `http.rs` | reference |
| 07 | `07-gatewayd-ws-handshake.md` | base64, the WebSocket handshake | `ws_handshake.rs` | reference |
| 08 | `08-gatewayd-ws-frames.md` | the frame decoder and encoder | `ws_frame.rs` (a naive decoder as oracle, 300 seeds) | reference |
| 09 | `09-gatewayd-ws-conn.md` | the connection: pings, close, a dead peer | `ws_conn.rs`, `support/ws_server.rs` | reference; 7 mutations, all caught once a test for the `Host` header was added |
| 10 | `10-gatewayd-mm.md` | Mattermost's events and REST calls | `mm_json.rs`, `mm_rest.rs`, `support/http_server.rs` | reference; 11 mutations, 10 caught, 1 that cannot change behaviour |
| 11 | `11-gatewayd-sessions.md` | routing, commands, the queue per session | `sessions.rs` | reference; 14 mutations, 13 caught, 1 that cannot change behaviour |
| 12 | `12-gatewayd-state.md` | `state.json` | `state.rs` | reference; 12 mutations, all caught |
| 13 | `13-gatewayd-deliver.md` | turns on `loop.sock`, answers in the thread | `deliver.rs`, `support/fake_loop.rs` | reference; 11 mutations, 10 caught; the one left accepts an event frame with another id, which `loopd` never sends |
| 14 | `14-gatewayd-serve.md` | the serve loop: routing, typing, catch-up, reconnecting | `serve.rs`, `serve_restart.rs`, `support/fake_mm.rs`, `support/gateway.rs` | reference; 14 mutations, all caught once four tests were added; 8 runs clean |
| 15 | `15-gatewayd-main.md` | `gatewayd serve --config <path>` | `main.rs` | reference; run against the owner's server, token never printed |
| 16 | `16-gatewayd-secret-file-race.md` | review finding 2: a secret file is read from the file that was checked | `secrets_race.rs` | reference in the working tree, then removed; 2 mutations, both caught; gate at the end state |
At the end of task 15: about 762 tests (650 before task 01). The design model's review fixes add 2; at the end of task 16: about 769.
## Changes during the run
- 2026-09-23, task 08: tasks 01 to 07 committed; task 08's session ended with nothing written. It
planned all four `todo!()`s in its head, most of it deliberating how to avoid indexing, and was
cut off (tip T25, again: `Decoder::header` has a dozen branches and was one `todo!()`). The
tree also had `handshake.rs` with its imports in 2021-edition order, which no logged command
explains; it was reverted. The attempt is saved in `.state/runs/M4a/08-first-attempt.diff`.
The design model split the skeleton: `next_message` and `header` are now written as glue, over
seven helpers of a few lines each whose comments give the exact expressions. The same was done
for task 09's `poll` (glue over four helpers) before it could stop the same way. Both new
skeletons were checked in a scratch copy of the branch: they compile and fail their tests, and
filled literally from their comments they pass (7 of 7, 10 of 10 five times), clippy clean, gate
ok. Resume from task 08.
- 2026-09-23, task 14: tasks 08 to 13 committed, each on its first session. Task 14's session
wrote nothing, not even the copies: it read twenty source files to learn the APIs, then planned
`connect`'s wait for `hello` in prose ("let me stop designing", and it did not) until it was cut
off. Twelve-plus `todo!()`s across two files and the whole crate's API were too much for one
turn. The design model wrote `connect`, `Gateway::new` and `catch_up` as glue (leaving
`catch_up_channel`), gave `handle_post` and `start` their borrow- and move-sensitive lines
verbatim, added a table of every call the helpers make with its signature (checked against the
branch), and made copying and seeing the tests fail the first two actions. Task 15's `serve`
comment, whose numbered steps had run together, was rewritten one step per item. Both were
checked in a scratch copy of the branch at task 14's start: the skeletons compile and fail
(`serve` 6, `serve_restart` 5 with 2 passing, `main` 4 with 1 passing), and filled from their
comments they pass (five runs), clippy clean, gate ok. Resume from task 14.
- 2026-09-24, task 14 again: this session copied the files and saw the tests fail, as told, then
deliberated until cut off, over a real contradiction: `post`'s comment was `now_ms`'s text.
The design model's script that rewrote the comments placed each after the first
eight-space comment following the function's name; `now_ms` is a free function with a
four-space comment, so its text went to `post`, and `post`'s was lost. The check filled the
bodies from the design model's own code, not from the comments, so it could not see this.
Both comments are fixed; every `todo!()` comment in tasks 14 and 15 was then read beside its
signature. Tasks 14 and 15 now say to stop and quote a comment that does not fit. The attempt is
saved in `.state/runs/M4a/14-second-attempt*`. Resume from task 14.
- 2026-09-24, after the review: the design model fixed findings 1 and 3 (commit `0081f24`), and
wrote task 16 for finding 2. Run it with `tools/run-plan.sh docs/plans/M4a 16`.
## Running it
```sh
git switch m4a
BOXMAKER_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M4a
```
Keep the OpenCode TUI closed while it runs.