M5b plan: README brought up to the written tasks

The task table follows the files (03 tags, 04 refuse, 05 flag) and
gives each task's gate count and mutations; the interfaces are the ones
the tasks built (task 02's role argument and SecretSpec, Refusal.at,
Flag, TurnDone.untrusted required, disarm, LockError::line); the review
focus names the tasks that pin each item; which task adds each runbook
entry, and the -taskNN convention for replaced given copies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:30:44 -07:00
co-authored by Claude Opus 5.5
parent 81d849d4ba
commit ba3c89791f
+49 -33
View File
@@ -37,14 +37,14 @@ Inputs the spec implies but no single task's tests pin down; each line names the
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.
a longer word. Task 03 measured each shape against the tokenizer and pins it: inside a word the
tag is still one token and is neutralised; upper case, zero-width, spaced or full-width forms
are ordinary tokens. Memory refuses the zero-width split as invisible (task 04).
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.
limit and then neutralised is a few bytes over. Task 07 pins the order (cap, then neutralise) and
that `truncated` comes from the cap alone.
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.
because its letters fold to Latin. Task 05's `sanitize_flag_edges.rs` holds 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
@@ -52,23 +52,29 @@ add it.
## Tasks
| # | File | Delivers | Crates | Check |
|---|---|---|---|---|
| 00 | (the design model, before the run) | `proto` → `bxcore`, compiler-driven, with the four layers of spec 3; `AGENTS.md`, `CLAUDE.md`, `docs/dependencies.md` and the brief updated | all | done: `2b3ae08`, `9672cb5`, `6798502`; gate 944 |
| 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.md` | the six runbook entries as given fragments | docs | gate's runbook check |
| # | File | Delivers | Crates | Gate after | Mutations |
|---|---|---|---|---|---|
| 00 | (the design model, before the run) | `proto` → `bxcore`, compiler-driven; `AGENTS.md`, `CLAUDE.md`, `docs/dependencies.md` and the brief updated | all | 944 | done: `2b3ae08`, `9672cb5`, `6798502` |
| 01 | `01-invalid-arguments-detail.md` | `ToolResponse::Denied.detail`; `brokerd` keeps the `ArgsError`; `loopd` appends it | bxcore, brokerd, loopd | 956 | 20 of 21 |
| 02 | `02-secrets-move.md` | `bxcore::secrets` behind the `secrets` feature; `gatewayd` uses it | bxcore, gatewayd, loopd (a guard test) | 963 | 23 |
| 03 | `03-sanitize-tags.md` | the 33 tags with their token ids (from the tokenizer, built from pieces), `sanitize::neutralise`, the measured shapes | bxcore | 981 | 16 |
| 04 | `04-sanitize-refuse.md` | `sanitize::refuse`: control, invisible, private use, noncharacters, known-prefix secrets (PGP private keys too), and tags | bxcore | 997 | 40 |
| 05 | `05-sanitize-flag.md` | `sanitize::normalise` and `sanitize::flag`: seven rules from Hermes (MIT, `d0288be`) and M5a's phrases | bxcore | 1023 | 60 |
| 06 | `06-memoryd-sanitize.md` | `memoryd` refuses and flags through `sanitize`; a flag keeps the text as untrusted notes; the block leaves out refused entries by rule name; `check.rs` goes; three runbook entries | memoryd | 1033 | 26 |
| 07 | `07-loopd-neutralise.md` | `loopd` neutralises user messages, tool results (after the cap) and the memory block when they are first logged | loopd | 1042 | 14 |
| 08 | `08-loopd-selftest-tags.md` | the self-test's check "tags tokenize as recorded" (66 `/tokenize` calls), and its runbook entry | loopd | 1050 | 20 |
| 09 | `09-turn-done-untrusted.md` | `TurnDone.untrusted`, required, from the session log's labels | bxcore, loopd | 1058 | 12 |
| 10 | `10-gatewayd-links.md` | images always as code; URLs as code with the notice when untrusted | gatewayd | 1081 | 71 |
| 11 | `11-bxctl-sanitize-check.md` | `bxctl sanitize check` | bxctl | 1099 | 21 |
| 12 | `12-instance-lock.md` | `bxcore::instance::lock`; `loopd` and `inferproxy` take it; runbook `already-running` | bxcore, loopd, inferproxy | 1113 | 24 of 25 |
| 13 | `13-instance-lock-rest.md` | `brokerd`, `memoryd` and `gatewayd` take it | brokerd, memoryd, gatewayd | 1118 | 14 |
| 14 | `14-runbook.md` | the runbook entry `sanitizer-rules`, and two entries pointed at it | docs | 1118 | none (docs) |
Each count is the gate's test total on titan (rustc 1.98.1) at the end of that task's reference.
The mutations are one-line breaks of the reference, each caught by a given test unless the row
says otherwise: task 01's missed one and task 12's (`TryLockError::Error` read as `Held`, which no
test can provoke) are known. Tasks 06 and 10 are the largest: 06 is 23 exact edits, 10 is 16 small
`todo!()` helpers in a Markdown scanner; watch them in the run.
After task 14, the design model runs the device check of spec 13 on straylight and the owner's
Mattermost, as for M5a.
@@ -78,15 +84,24 @@ Mattermost, as for M5a.
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`.
- Task 02: `bxcore::secrets::{Secret, SecretSource, SecretSpec, load, read_checked}`, feature
`secrets`; `load` and `read_checked` take the role's name.
- 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 }`.
- `TAGS: &[Tag]` (each with its text and token id) and `neutralise(text: &str) -> Cow<'_, str>`;
- `refuse(text: &str) -> Option<Refusal>`, `Refusal { rule, sentence, at }` (`at` is a byte
offset; the first rule only);
- `normalise(text: &str) -> String` and `flag(text: &str) -> Option<Flag>`, `Flag { rule }`.
- Task 09: `bxcore::TurnDone { …, untrusted: bool }`, required on the wire.
- Task 10: `gatewayd::links::disarm(text, untrusted) -> Disarmed { text, links }`.
- Task 12: `bxcore::instance::lock(path: &Path) -> Result<InstanceLock, LockError>`, with
`LockError::Held` and `LockError::Io`.
`LockError::Held`, `LockError::Io` and `LockError::line(role, path)`.
Runbook entries, and the task that adds each (the gate fails on a pointer without its entry):
`memory-write-refused`, `memory-write-flagged` and `memory-entry-excluded` (06),
`loopd-tags-unexpected` (08), `already-running` (12), `sanitizer-rules` (14).
Given copies replaced by a later task keep the earlier version as `<name>-taskNN`, named for the
task that copies it (`broker.rs-task01`, `serve.rs-task01`).
## Before hand-over
@@ -95,9 +110,10 @@ each task's end state on titan's toolchain, in a scratch worktree with its own b
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).
Facts fetched while writing the tasks: the tag list and token ids from the GGUF on straylight,
each checked with `/tokenize` (task 03); Hermes' `tools/threat_patterns.py` at `d0288be` and its
MIT licence (task 05); llama-server's `/tokenize` fields at b10964 (task 08); Mattermost's
Markdown, from its `marked` fork and server parser (task 10).
## Running it