Task 08's `header` was one todo!() with a dozen branches; Ornith planned it in its head until the turn ran out (tip T25). `next_message` and `header` are now written as glue over seven small helpers, and task 09's `poll` over four, each checked to pass when filled from its comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
90 lines
6.3 KiB
Markdown
90 lines
6.3 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 |
|
|
|
|
At the end of task 15: about 762 tests (650 before task 01).
|
|
|
|
## 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.
|
|
|
|
## 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.
|