From 727e027fcf97425b5814a9fdec51d0c52a4e4b25 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 26 Sep 2026 01:42:26 -0700 Subject: [PATCH] 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) --- docs/plans/M5b/README.md | 111 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/plans/M5b/README.md diff --git a/docs/plans/M5b/README.md b/docs/plans/M5b/README.md new file mode 100644 index 0000000..c2c4987 --- /dev/null +++ b/docs/plans/M5b/README.md @@ -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**: `<` 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 }`. +- Task 02: `bxcore::secrets::{Secret, SecretSource, load}`, feature `secrets`. +- Tasks 03 to 05, in `bxcore::sanitize`: + - `refuse(text: &str) -> Option`, 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` (the rule's name). +- Task 09: `bxcore::TurnDone { content, usage, untrusted: bool }`. +- Task 12: `bxcore::instance::lock(path: &Path) -> Result`, 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.