# 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 ` 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 ` | `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). ## 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.