diff --git a/docs/specs/2026-09-24-m5a-local-memory.md b/docs/specs/2026-09-24-m5a-local-memory.md index 7630f15..23f5cb7 100644 --- a/docs/specs/2026-09-24-m5a-local-memory.md +++ b/docs/specs/2026-09-24-m5a-local-memory.md @@ -175,7 +175,8 @@ FTS5's `bm25`. **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 -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` @@ -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): - `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` or `user.md`. -- In a session that **has**, an identity `add` becomes a `note`, and the answer says so: `saved as - a note (untrusted): this session has read untrusted content`. It 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. +- In a session that **has**, an identity `add` becomes a `note`, and the answer says so, naming + the file it did not change: `not added to memory.md: this session has read untrusted content, so + it was saved in notes.md as an untrusted note` (or `user.md`). On the device (2026-09-25) the + model read the earlier, shorter answers as success and told the owner the file was changed. + 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): @@ -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 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 - runtime's: the higher class, and untrusted if either is. The audit record and the session's - taint use the combined label, as today. + runtime's: the higher class, and untrusted if either is. The audit record carries the combined + 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 before anything is added. @@ -462,7 +470,8 @@ Offline, in `make gate`: 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 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` sent after a finished turn and not after a turn ending in tool calls. - `bxctl`: `reindex` and each `memory` command against a fake socket.