docs: runbook: sanitizer-rules, how to change a sanitizer rule

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-27 19:08:42 -07:00
co-authored by ornith-1.5-35b-a3b
parent 7cbc1d7342
commit 27a6354a80
2 changed files with 47 additions and 4 deletions
+46 -4
View File
@@ -672,8 +672,8 @@ standard input) names each rule that fires and writes nothing. The session's log
arguments.
**Fix.** A real secret: rotate it, since it has been in the conversation; keep it out of memory.
Ordinary text refused by mistake: add it to the code-work corpus and narrow the rule, as the M5b
spec's section 6.5 says; the change reaches the agent when it is rebuilt.
Ordinary text refused by mistake: add it to the code-work corpus and narrow the rule, as
[`sanitizer-rules`](#sanitizer-rules) says; the change reaches the agent when it is rebuilt.
**Check.** `bxctl sanitize check` with the text prints nothing and exits 0, and the agent's next
write of it is answered `… updated` or `saved in notes.md`.
@@ -703,8 +703,8 @@ write is not staged: it is a note at once. The log names the rule, never the tex
reaches the prompt. If it is a true fact wrongly flagged, move it into identity by hand: add it to
`<home>/memory/memory.md` (or `user.md`) with an editor, as an entry of its own, with a line
holding only `§` between it and the entry before, keeping the file within its limit. The note
stays in `notes.md`, which is append-only. If the rule catches ordinary text, narrow it as the M5b
spec's section 6.5 says.
stays in `notes.md`, which is append-only. If the rule catches ordinary text, narrow it as
[`sanitizer-rules`](#sanitizer-rules) says.
**Check.** Start a new conversation and ask the agent what `memory.md` holds; the entry is there.
@@ -830,3 +830,45 @@ does no harm, and does not help either.
**Check.** `pgrep -x <role>` prints one process id, and the daemon prints its start line
(`loopd: serving on …`, `inferproxy: <socket> -> <upstream>`, `brokerd: serving tools on …`,
`memoryd: serving`, `gatewayd: connected to …`).
## 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.