memoryd: refuse and flag memory writes through bxcore::sanitize

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 b3e9af7586
commit ca8b50cd69
15 changed files with 1001 additions and 249 deletions
+77
View File
@@ -651,3 +651,80 @@ fails too, stop `memoryd`, move `index/memory.sqlite3` aside, and start `memoryd
it.
**Check.** `bxctl reindex` prints how many logs, records and notes it read.
## memory-write-refused
**What you see.** The agent says memory refused something: its `memory` call was answered
`memory refused this entry: it contains …` (what looks like a GitHub token, an invisible or
direction-changing character, and so on), and `memoryd` logged `memoryd: a memory write from
session <id> was refused by the rule <rule>; see docs/runbook.md#memory-write-refused`. Nothing
was written to `memory/`, staged for review or journalled.
**Why.** Every text a memory write would store is checked against the sanitizer's refuse rules
(M5b spec, section 6.1): control characters other than line feed and tab, invisible and
direction-changing characters, private-use characters, noncharacters, secrets known by their
prefix (GitHub, GitLab, `sk-` keys, Slack, AWS, private-key blocks), and the model's
chat-template tags. Such text is only ever an attack or a leaked secret, and identity goes into
every prompt. The log names the rule, never the text.
**Confirm.** The rule in the log line. `bxctl sanitize check` with the text (as an argument or on
standard input) names each rule that fires and writes nothing. The session's log holds the call's
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.
**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`.
## memory-write-flagged
**What you see.** A memory write was kept one tier lower. The `memory` call was answered `not
added to memory.md: it matches the rule <rule>, so it was saved in notes.md as an untrusted note`
(or `user.md`), or, for a note, `saved in notes.md, marked untrusted: it matches the rule <rule>`.
`memoryd` logged `memoryd: a memory write from session <id> matches the rule <rule>; it was saved
in notes.md as an untrusted note (edit <edit id>); see docs/runbook.md#memory-write-flagged`, and
`bxctl memory log` shows that edit with `flagged <rule>`.
**Why.** The text is shaped like an attack (M5b spec, section 6.3): it tells the reader to ignore
or replace earlier instructions, to send data to a URL, to dump the conversation, or to write into
SSH configuration, or it holds one of M5a's six instruction phrases. A flag never refuses and
never loses text: `memory.md` and `user.md`, which are in every prompt, are not changed, and every
text of the write is saved at the end of `notes.md` under an `untrusted` header. `recall` returns
it as `memory-tainted`, so a session that recalls it becomes untrusted. With review on, a flagged
write is not staged: it is a note at once. The log names the rule, never the text.
**Confirm.** `bxctl memory log`: the edit, `flagged <rule>`. The text is at the end of
`<home>/memory/notes.md`, under a line `§ noted <time> in session <id> · <class> · untrusted`.
`bxctl sanitize check` with the text names the rule.
**Fix.** If it is an attack, leave it: the note is the record of what was tried, and it never
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.
**Check.** Start a new conversation and ask the agent what `memory.md` holds; the entry is there.
## memory-entry-excluded
**What you see.** `memoryd` logged `memoryd: an entry of memory.md was left out of the block by
the rule <rule>; see docs/runbook.md#memory-entry-excluded` (or `user.md`), each time a session
starts. Sessions start as usual, but the agent does not know that entry.
**Why.** When a session starts, every entry of `memory.md` and `user.md` is checked again against
the sanitizer's refuse rules (M5b spec, section 7), and an entry that breaks one is left out of
the prompt. So a rule added later, or an entry an editor put in by hand, protects at once, with no
migration. The file itself is not changed. Flag rules are not checked here, so an entry you moved
from `notes.md` by hand stays in, and `soul.md` is the owner's and is not checked at all.
**Confirm.** The rule in the log line names the kind of text (`secret-github`, `invisible`, …).
Run each entry of the file through `bxctl sanitize check`: the one it reports is the one left out.
**Fix.** Edit the file with an editor: remove the entry, or the part of it that breaks the rule.
Entries are separated by a line holding only `§`. A secret does not belong in memory: rotate it
if it was real.
**Check.** Start a new conversation: `memoryd` does not log the line again.