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>
112 lines
7.4 KiB
Markdown
112 lines
7.4 KiB
Markdown
# 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<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.
|