# M5a task 21: a session's state follows the result's own label **Branch:** `m5a` (run `git switch m5a`; `git status --short` must be empty, otherwise stop) **Commit subject:** `brokerd: the ledger raises a session's state by the stricter label` ## Goal A fix for a hole the M5a device check found (2026-09-25). After every tool call, the ledger raises the session's state (its taint and whether it has read untrusted content). It used the label of the **grant** the call ran under, never the label on the result. Before M5a they were always the same. Now `memoryd` can make a result stricter: a `recall` that finds a `memory-tainted` note answers `untrusted`. The runner already combines the two labels, and the audit log records the combined one, but the session stayed trusted. So a clean session could recall a note written after a fetched page, then `add` it to `memory.md` as identity. On the device, that is what happened. The fix: in `Ledger::finish`, raise the state by the stricter of the grant's label and the result's own: the higher class, and untrusted if either is (M5a spec, section 8, as changed in `0e5b1d2`). ## Files - Copy: the new test `crates/brokerd/tests/ledger_label.rs`, from `docs/plans/M5a/files/` (the same path under it) - Modify: `crates/brokerd/src/ledger/answer.rs`, `docs/implementer-log.md` ## The edits, exactly Both are in `crates/brokerd/src/ledger/answer.rs`. Nothing else in the file changes. **1.** The line ```rust use crate::policy::{Ask, Denial, SessionState}; ``` becomes ```rust use crate::policy::{Ask, Denial, Label, SessionState}; ``` **2.** In `finish`, the lines ```rust // 3. A result raises the state, then is recorded. ToolResponse::Result { .. } => { let raised = match self.state_read(&guard, &call.session) { Ok(state) => match guard.state.raise(&call.session, state, call.label) { ``` become ```rust // 3. A result raises the state, then is recorded. The state follows the stricter of // the grant's label and the result's own: `memoryd` can make a result stricter. ToolResponse::Result { class, untrusted, .. } => { let label = Label { class: call.label.class.max(*class), untrusted: call.label.untrusted || *untrusted, }; let raised = match self.state_read(&guard, &call.session) { Ok(state) => match guard.state.raise(&call.session, state, label) { ``` The lines after them (`Ok(raised) => raised,` and on) stay as they are. ## Steps - [ ] **1. Copy the test:** `cp docs/plans/M5a/files/crates/brokerd/tests/ledger_label.rs crates/brokerd/tests/` - [ ] **2. See it fail.** `cargo test -p brokerd --test ledger_label`. Expected: two tests fail, `a_recall_of_a_tainted_note_makes_a_clean_session_untrusted` and `a_result_of_a_higher_class_than_its_grant_raises_the_taint`. The third, `a_result_laxer_than_its_grant_still_raises_the_state_by_the_grant`, already passes; it guards what must not change. - [ ] **3. Make the two edits.** Run `cargo fmt --all`. - [ ] **4. See it pass.** `cargo test -p brokerd --test ledger_label`: all three pass. Then `cargo test -p brokerd`: every suite passes. - [ ] **5. Run the gate.** `make gate`, with a shell timeout of at least 600000 ms: on titan, while it serves Ornith, the gate takes longer than 2 minutes. Expected last line: `gate: ok`, with 944 tests (941 before this task, plus the three new ones). - [ ] **6. Log and commit.** `git add crates/brokerd docs/implementer-log.md && git commit` ## Done when - `make gate` prints `gate: ok` with 944 tests. ## Stop and report if - Either quoted block is not in the file exactly as shown. - Step 2 shows a different set of failing tests than the two named. - A test other than these three fails after the edits. Do not change a test.