21: the ledger raises a session's state by the stricter of the grant's label and the result's own, so a clean session that recalls a memory-tainted note becomes untrusted and cannot write it into memory.md (found on straylight, 2026-09-25). 22: the answers to a note name notes.md, and for an untrusted add the file that was not changed. The new given tests are ledger_label.rs and new versions of memoryd's edit.rs and serve.rs; the old copies are kept as -task06 and -task10. On a scratch tree at the end of task 20: the named tests fail and then pass, 5 and 4 mutations are caught, and the gate passes with 944 tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
95 lines
3.9 KiB
Markdown
95 lines
3.9 KiB
Markdown
# 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.
|