M5b plan: the outline (README), for the owner's review of the breakdown

Task 00 (proto -> bxcore) by the design model before the run; fourteen
Ornith tasks: the invalid-arguments detail, the secrets move, the
sanitizer in three tasks, memoryd, loopd's neutralising and self-test,
TurnDone.untrusted, gatewayd's links, bxctl sanitize check, the instance
lock in two tasks, and the runbook. Global constraints, five review-focus
inputs each owned by a task, and the interfaces between tasks. The task
files and the reference come next, through docs/plans/CHECKLIST.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 01:42:26 -07:00
co-authored by Claude Opus 5.5
parent a13d584f7a
commit 727e027fcf
+111
View File
@@ -0,0 +1,111 @@
# M5b implementation plan: hardening
> For the implementer: each task file is one closed task for one fresh session. Read `AGENTS.md`,
> then the task file, and do exactly that task. The driver is `tools/run-plan.sh`.
**Goal:** Boxmaker refuses, neutralises or flags hostile text on every path into the model and into
memory, keeps its secrets in one shared store, explains refused tool calls, disarms links in
replies from untrusted sessions, and never runs two copies of a daemon.
**Architecture:** `proto` becomes `bxcore`, the one crate every role may depend on. It gains
`secrets` (moved from `gatewayd`) and `sanitize` (refuse, neutralise, flag). The roles call it:
`memoryd` refuses and flags memory writes, `loopd` neutralises tags when text is first logged and
checks the tag list against the tokenizer at startup, `gatewayd` escapes images and disarms links,
`bxctl` explains a text. Each daemon holds a lock file for its life.
**Tech stack:** Rust 1.95, edition 2024, the standard library (`File::try_lock` for the locks); no
new crate.
**Spec:** `docs/specs/2026-09-26-m5b-hardening.md` (approved 2026-09-26). Section numbers below are
the spec's.
## Global constraints
- `rust-version = "1.95"`; no `unsafe`; no async runtime; no new dependency.
- Roles depend on `bxcore` only; `bxcore` depends on no workspace crate (spec 3).
- The `secrets` module is behind the Cargo feature `secrets`; `loopd` never enables it (spec 5).
- No file the implementer reads holds a chat-template tag whole; tags are built from pieces
(`concat!`) and named in words (T33).
- Every path a given test creates is removed by a guard (T44).
- Rules and lists are compiled in; no rules file (spec 6.5).
- Every new fail-closed state has a runbook entry and a `see docs/runbook.md#…` pointer.
- Commit trailer: the `Co-Authored-By` line the driver gives; gate with a 600000 ms timeout.
## Review focus
Inputs the spec implies but no single task's tests pin down; each line names the task whose tests
add it.
1. **Tags in other shapes**: a listed tag in upper case, split by a zero-width character, or inside
a longer word. Neutralising matches the listed spellings exactly; the zero-width split is
refused in memory (spec 6.1) but reaches `loopd` in a tool result. Task 04 pins what happens to
each shape, so the behaviour is decided, not accidental.
2. **Neutralising grows text past a cap**: `&lt;` is longer than `<`, so a tool result capped at its
limit and then neutralised is a few bytes over. Task 08 pins the order (cap, then neutralise) and
that the log records the result's true length.
3. **Non-Latin text and the homoglyph map**: a note written in Russian or Greek must not be flagged
because its letters fold to Latin. Task 05's code-work corpus includes such notes.
4. **Links already in code, and Markdown links**: in an untrusted reply, a URL already inside a code
span or fence is not wrapped twice, and `[text](url)` is disarmed as a whole. Task 10 pins both.
5. **A lock file that cannot be created** (an unwritable `run/`, or `inferproxy`'s socket directory
read-only): the daemon does not start and names the error. Task 12 pins it.
## Tasks
| # | File | Delivers | Crates | Check |
|---|---|---|---|---|
| 00 | (the design model, before the run) | `proto` → `bxcore`, compiler-driven, with the four layers of spec 3 | all | gate; not an Ornith task (it spans every crate) |
| 01 | `01-invalid-arguments-detail.md` | `ToolResponse::Denied.detail`; `brokerd` keeps the `ArgsError`; `loopd` appends it | bxcore, brokerd, loopd | fixture; reference |
| 02 | `02-secrets-move.md` | `bxcore::secrets` behind the `secrets` feature; `gatewayd` uses it | bxcore, gatewayd | the moved tests; reference |
| 03 | `03-sanitize-refuse.md` | `sanitize::refuse`: control, invisible, private use, noncharacters, known-prefix secrets, and tags in memory | bxcore | corpora (refuse part); mutations |
| 04 | `04-sanitize-tags.md` | the tag list (from the tokenizer, built from pieces), `sanitize::neutralise`, and the decided handling of review-focus 1 | bxcore | corpora (neutralise part); mutations |
| 05 | `05-sanitize-flag.md` | `sanitize::normalise` and `sanitize::flag`, Hermes' strict patterns (MIT) and M5a's phrases | bxcore | corpora (flag part, incl. non-Latin notes); mutations |
| 06 | `06-memoryd-sanitize.md` | `memoryd` refuses and flags through `sanitize`; a flag keeps the text as an untrusted note; the block leaves out refused entries by rule name; `check.rs` goes | memoryd | given tests; mutations |
| 07 | `07-loopd-neutralise.md` | `loopd` neutralises user messages, tool results (after the cap) and the memory block when they are logged | loopd | given tests; replay byte-identical |
| 08 | `08-loopd-selftest-tags.md` | the self-test's fourth check, against a fake tokenizer | loopd | given tests |
| 09 | `09-turn-done-untrusted.md` | `TurnDone.untrusted` from the session's labels | bxcore, loopd | fixture; given tests |
| 10 | `10-gatewayd-links.md` | images escaped always; URLs in code spans with the notice when untrusted | gatewayd | given tests incl. review-focus 4 |
| 11 | `11-bxctl-sanitize-check.md` | `bxctl sanitize check` | bxctl | given tests |
| 12 | `12-instance-lock.md` | `bxcore::instance::lock`, and `loopd` and `inferproxy` take it | bxcore, loopd, inferproxy | given tests incl. review-focus 5 |
| 13 | `13-instance-lock-rest.md` | `brokerd`, `memoryd` and `gatewayd` take it | brokerd, memoryd, gatewayd | given tests |
| 14 | `14-runbook-and-rules.md` | the six runbook entries as given fragments; `AGENTS.md` and `CLAUDE.md` say `bxcore` | docs | gate's runbook check |
After task 14, the design model runs the device check of spec 13 on straylight and the owner's
Mattermost, as for M5a.
## Interfaces between tasks
Names later tasks rely on; each task file gives the full signatures.
- Task 01: `bxcore::ToolResponse::Denied { reason: DenyReason, detail: Option<String> }`.
- Task 02: `bxcore::secrets::{Secret, SecretSource, load}`, feature `secrets`.
- Tasks 03 to 05, in `bxcore::sanitize`:
- `refuse(text: &str) -> Option<Refusal>`, where `Refusal` carries the rule's name and sentence
(only memory writes use it in M5b; M5c's outgoing turns may add a narrower variant);
- `neutralise(text: &str) -> Cow<'_, str>` and `TAGS: &[Tag]` (each with its token id);
- `normalise(text: &str) -> String` and `flag(text: &str) -> Option<Flag>` (the rule's name).
- Task 09: `bxcore::TurnDone { content, usage, untrusted: bool }`.
- Task 12: `bxcore::instance::lock(path: &Path) -> Result<InstanceLock, LockError>`, with
`LockError::Held` and `LockError::Io`.
## Before hand-over
Every task is worked through `docs/plans/CHECKLIST.md`: a reference replayed through the gate at
each task's end state on titan's toolchain, in a scratch worktree with its own build directory;
mutations; given tests walked by hand and grepped against earlier given files; `/tmp` compared
around a gate run; the reference moved to straylight as a bundle before the run.
Facts to fetch while writing the tasks, not guessed: the tag list and token ids from Ornith's
tokenizer configuration on straylight (task 04); Hermes' `tools/threat_patterns.py` and its
licence (task 05).
## Running it
```sh
git switch -c m5b master # after task 00 is committed on master
OPENCODE_MODEL=titan/ornith-1.5-35b-a3b RESUME=1 \
SLOTS_URL='http://127.0.0.1:8081/slots?model=ornith-1.5-35b-a3b' \
tools/run-plan.sh docs/plans/M5b 01
```
`RESUME` and the watchdog are trials on this run (decisions, 2026-09-26); the review judges them.