M5b plan: task 14, the runbook entry sanitizer-rules

The last runbook entry, the rule-change procedure of spec 6.5; the
entries memory-write-refused and memory-write-flagged now send the
reader to it instead of to the spec. Tasks 06, 08 and 12 added the
other five, because the gate needs an entry for every pointer.

Checks: the edits, applied from the task's text to ref 13, give a
runbook byte-identical to the reference; each before block found once;
the gate passes on the reference with 1118 tests; /private/tmp unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:29:50 -07:00
co-authored by Claude Opus 5.5
parent 5f48ba65b0
commit 81d849d4ba
2 changed files with 119 additions and 0 deletions
@@ -0,0 +1,41 @@
## sanitizer-rules
**What you see.** Not a failure: the procedure for changing a sanitizer rule, which the entries
`memory-write-refused` and `memory-write-flagged` send you to when a rule catches ordinary text,
or which you follow when an attack gets through. The rules are in `crates/bxcore/src/sanitize/`:
`refuse.rs` (6.1 of the M5b spec), `tags.rs` (the chat-template tags that are neutralised, 6.2)
and `flag.rs` with `normalise.rs` (6.3).
**Why.** The rules are compiled in rather than read from a file, so that every change passes the
two corpora and review, and a typo cannot switch a rule off (M5b spec, section 6.5). A rule that
refuses code is as much a bug as one that lets an attack through: the agent works on code
projects, and a flagged note makes every session that recalls it untrusted.
**Confirm.** The evidence, before anything changes:
- a rule's name in `bxctl memory log` (`flagged <rule>`), in `memoryd`'s log, or in a refusal the
agent passed on;
- `bxctl sanitize check` with the text, which names each rule that fires and writes nothing;
- a device check, a review or new research that shows an attack the rules miss.
**Fix.** In this order, one commit:
1. **The case first.** Add the text to a corpus, with a comment saying where it was seen: the
attack corpus if it must be caught, the code-work corpus if it must pass. The corpora are the
given tests `crates/bxcore/tests/sanitize_refuse_corpus.rs` (refuse),
`sanitize_corpus.rs` (neutralise) and `sanitize_flag_corpus.rs` (flag). Build a tag or a key
from pieces, as the files already do: never write one whole. Run `cargo test -p bxcore` and see
the new case fail.
2. **The rule.** Add or narrow the entry in the rule's table, with a comment naming its source.
Keep it as narrow as the evidence: a flag rule ported from Hermes keeps its attribution.
3. **The gate.** `make gate` runs both corpora for every action, so a rule that catches code
fails before it lands.
4. **The record.** A policy change (a new kind of refusal, or a rule moved from flag to refuse)
gets a row in `docs/decisions.md`; an ordinary rule change is its commit.
A change reaches an agent when it is rebuilt and restarted. Stored memory is never rewritten: an
identity entry that breaks a new refuse rule leaves the memory block at once (see
`memory-entry-excluded`), and a note flagged under an old rule stays as it was written.
**Check.** `bxctl sanitize check` with the text gives the new answer (nothing, and exit 0, for a
text that must pass). After the rebuild, the agent's next write of it is answered as expected.