M5a spec: the session's state follows a result's own label; plainer note answers

Found on the device (2026-09-25): the ledger raised a session's state by
the grant's label, so a clean session that recalled a memory-tainted note
stayed trusted and could add it to memory.md. The spec's "as today" was
wrong; section 8 now says the state is raised by the stricter of the
grant's label and the result's, and the recall section says the session
becomes untrusted. The note answers (section 3) name notes.md and, for an
untrusted add, the file that was not changed: the model read the old ones
as success. Tasks 21 and 22.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 17:44:35 -07:00
co-authored by Claude Opus 5.5
parent a3c800cf91
commit 0e5b1d2820
+19 -10
View File
@@ -175,7 +175,8 @@ FTS5's `bm25`.
**The label.** The result's class is the highest class among the hits (at least `private`), and **The label.** The result's class is the highest class among the hits (at least `private`), and
it is untrusted if any hit is tagged `memory-tainted`. `brokerd` combines that with the grant's label and keeps the it is untrusted if any hit is tagged `memory-tainted`. `brokerd` combines that with the grant's label and keeps the
stricter of each (section 8). stricter of each (section 8), and the session's state follows the combined label: a session that
recalls a `memory-tainted` hit becomes untrusted, so its identity writes become notes from then on.
### `memory` ### `memory`
@@ -203,14 +204,17 @@ the file fails safely: every `old` must match an entry of the file as it is now,
**Where a write goes.** `brokerd` sends each call with the session's state (section 7): **Where a write goes.** `brokerd` sends each call with the session's state (section 7):
- `note` is appended to `notes.md` in any session, labelled by the session (above). The answer: - `note` is appended to `notes.md` in any session, labelled by the session (above). The answer:
`noted (untrusted)` or `noted`. `saved in notes.md`, or in a session that has read untrusted content `saved in notes.md, marked
untrusted: this session has read untrusted content`.
- In a session that has **not** seen untrusted content, the identity actions apply to `memory.md` - In a session that has **not** seen untrusted content, the identity actions apply to `memory.md`
or `user.md`. or `user.md`.
- In a session that **has**, an identity `add` becomes a `note`, and the answer says so: `saved as - In a session that **has**, an identity `add` becomes a `note`, and the answer says so, naming
a note (untrusted): this session has read untrusted content`. It is remembered and `recall` the file it did not change: `not added to memory.md: this session has read untrusted content, so
finds it; it never reaches the prompt on its own. `replace`, `remove` and `consolidate` are it was saved in notes.md as an untrusted note` (or `user.md`). On the device (2026-09-25) the
refused with `this session has read untrusted content; it can take notes but not change model read the earlier, shorter answers as success and told the owner the file was changed.
memory.md`. The owner can move a note into a file by editing it. The note is remembered and `recall` finds it; it never reaches the prompt on its own.
`replace`, `remove` and `consolidate` are refused with `this session has read untrusted content;
it can take notes but not change memory.md`. The owner can move a note into a file by editing it.
**Rules**, checked by `brokerd` when parsing (shape) and by `memoryd` when applying (content): **Rules**, checked by `brokerd` when parsing (shape) and by `memoryd` when applying (content):
@@ -372,8 +376,12 @@ connection, the existing frames (`proto::frame`, `MAX_FRAME`). New message kinds
- **Labels only get stricter.** `Runtime` gains `run_labelled`, returning an optional label with - **Labels only get stricter.** `Runtime` gains `run_labelled`, returning an optional label with
the output; its default calls `run` and returns none, so container runtimes and the given test the output; its default calls `run` and returns none, so container runtimes and the given test
runtimes are unchanged. `runner::run` answers with the stricter of the grant's label and the runtimes are unchanged. `runner::run` answers with the stricter of the grant's label and the
runtime's: the higher class, and untrusted if either is. The audit record and the session's runtime's: the higher class, and untrusted if either is. The audit record carries the combined
taint use the combined label, as today. label. The ledger raises the session's state by the stricter of the grant's label and the
result's own, never by the grant's alone. (The first version of this spec said "as today", but
the ledger raised the state by the grant's label, which a result could not change before M5a. A
clean session could then recall a tainted note and write it into `memory.md`; found on the
device, 2026-09-25.)
- **Size.** `policy.rs` (498 lines) and `ledger.rs` (487) are at the limit; the plan splits them - **Size.** `policy.rs` (498 lines) and `ledger.rs` (487) are at the limit; the plan splits them
before anything is added. before anything is added.
@@ -462,7 +470,8 @@ Offline, in `make gate`:
from `progress`; a reindex equal to the incremental index; the recall log. from `progress`; a reindex equal to the incremental index; the recall log.
- `brokerd`: parsing both tools; the built-in grants present with an empty `grants/`; a grant - `brokerd`: parsing both tools; the built-in grants present with an empty `grants/`; a grant
file naming either tool refused at load; the session's state carried in `memory_call`; the composite runtime; the file naming either tool refused at load; the session's state carried in `memory_call`; the composite runtime; the
stricter label, with a runtime that returns a stricter and a laxer one. stricter label, with a runtime that returns a stricter and a laxer one; the session's state
raised by a result stricter than its grant, and by the grant when a result is laxer.
- `loopd`: the baseline with a fake `memoryd`; the refusal and its pointer; `session_updated` - `loopd`: the baseline with a fake `memoryd`; the refusal and its pointer; `session_updated`
sent after a finished turn and not after a turn ending in tool calls. sent after a finished turn and not after a turn ending in tool calls.
- `bxctl`: `reindex` and each `memory` command against a fake socket. - `bxctl`: `reindex` and each `memory` command against a fake socket.