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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user