entry_problem refuses any line that starts with the separator, and a noted header with an unknown last part is not a header. The new given tests are forgery.rs and new versions of memory_args.rs and notes.rs; the old copies are kept as -task02 and -task06. On a scratch tree at the end of task 17, the three tests named in the task fail and then pass, four mutations of the fix are caught, and the gate passes with 939 tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
120 lines
4.7 KiB
Markdown
120 lines
4.7 KiB
Markdown
# M5a task 18: a note cannot forge a header in `notes.md`
|
|
|
|
**Branch:** `m5a` (run `git switch m5a`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `proto, memoryd: no line of an entry may start with §, and headers are exact`
|
|
|
|
## Goal
|
|
|
|
A fix for a hole the M5a review found (2026-09-24). Each entry of `notes.md` starts with a header
|
|
line saying where it came from, such as
|
|
|
|
```text
|
|
§ noted 2026-09-24T21:02Z in session mm-web · private · untrusted
|
|
```
|
|
|
|
A session that has read untrusted content may still take notes. Until now a note's text could hold
|
|
a line shaped like a header, such as `§ moved 2026-09-24T20:14Z from memory.md in session
|
|
mm-owner`. Read back, that line started an entry of its own, and `recall` showed the text after it
|
|
tagged `identity`, not untrusted. Two changes (M5a spec, section 3, as changed in `4ae827a`):
|
|
|
|
1. **`proto`**: `entry_problem` refuses any line that starts with `§` once its leading spaces are
|
|
skipped, not only a line that is `§` alone. `brokerd` and `memoryd` both call it through
|
|
`MemoryArgs::check`, so it covers every entry and every note.
|
|
2. **`memoryd`**: in `notes.rs`, a `noted` header whose last part is anything other than
|
|
`untrusted` is not a header. Until now it was read as a trusted note.
|
|
|
|
## Files
|
|
|
|
- Copy: **new versions** of the given `crates/proto/tests/memory_args.rs` and
|
|
`crates/memoryd/tests/notes.rs`, and the new test `crates/memoryd/tests/forgery.rs`, all from
|
|
`docs/plans/M5a/files/` (the same paths under it)
|
|
- Modify: `crates/proto/src/memory.rs`, `crates/memoryd/src/notes.rs`, `docs/implementer-log.md`
|
|
|
|
## The edits, exactly
|
|
|
|
Nothing else in these files changes.
|
|
|
|
**1. `crates/proto/src/memory.rs`**, the doc comment above `pub fn entry_problem`: the two lines
|
|
|
|
```rust
|
|
/// Why an entry's text is not a well-formed entry: empty once trimmed, too long, or holding a line
|
|
/// that is only the separator. `None` when it is fine.
|
|
```
|
|
|
|
become the three lines
|
|
|
|
```rust
|
|
/// Why an entry's text is not a well-formed entry: empty once trimmed, too long, or holding a line
|
|
/// that starts with the separator, spaces aside. In `notes.md` such a line would read as a header,
|
|
/// and a header says where an entry came from. `None` when it is fine.
|
|
```
|
|
|
|
and in the body of `entry_problem`, the lines
|
|
|
|
```rust
|
|
if line.trim() == SEPARATOR {
|
|
return Some(format!(
|
|
"an entry cannot hold a line that is only {SEPARATOR}"
|
|
));
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
if line.trim_start().starts_with(SEPARATOR) {
|
|
return Some(format!(
|
|
"an entry cannot hold a line that starts with {SEPARATOR}"
|
|
));
|
|
```
|
|
|
|
The `for` loop around them and the closing braces stay.
|
|
|
|
**2. `crates/memoryd/src/notes.rs`**, in `parse_header`, the lines
|
|
|
|
```rust
|
|
let untrusted = match parts.get(2) {
|
|
None => false,
|
|
Some(s) => *s == "untrusted",
|
|
};
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
let untrusted = match parts.get(2) {
|
|
None => false,
|
|
Some(&"untrusted") => true,
|
|
Some(_) => return None,
|
|
};
|
|
```
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy the three test files:**
|
|
`cp docs/plans/M5a/files/crates/proto/tests/memory_args.rs crates/proto/tests/`, then
|
|
`cp docs/plans/M5a/files/crates/memoryd/tests/notes.rs docs/plans/M5a/files/crates/memoryd/tests/forgery.rs crates/memoryd/tests/`.
|
|
Do not copy the files ending `-task02` or `-task06`; they are the older versions.
|
|
- [ ] **2. See them fail.** `cargo test --no-fail-fast -p proto --test memory_args -p memoryd --test notes --test forgery`.
|
|
Expected: three tests fail, `entries_are_not_empty_not_too_long_and_hold_no_separator_line`,
|
|
`a_noted_header_with_an_unknown_last_part_is_not_a_header` and
|
|
`a_note_holding_a_header_line_is_refused_and_nothing_is_written`. The fourth new test,
|
|
`recall_finds_no_identity_in_what_an_untrusted_session_wrote`, already passes; it guards what
|
|
must not change.
|
|
- [ ] **3. Make the two edits.** Run `cargo fmt --all`.
|
|
- [ ] **4. See them pass.** The command from step 2: every test passes. Then
|
|
`cargo test -p proto -p memoryd -p brokerd`: every suite passes.
|
|
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`, with 939 tests (936 before
|
|
this task, plus the three new tests: one in `notes.rs`, two in `forgery.rs`).
|
|
- [ ] **6. Log and commit.**
|
|
`git add crates/proto crates/memoryd docs/implementer-log.md && git commit`
|
|
|
|
## Done when
|
|
|
|
- `make gate` prints `gate: ok` with 939 tests.
|
|
|
|
## Stop and report if
|
|
|
|
- Any line quoted above is not in the file exactly as shown.
|
|
- Step 2 shows a different set of failing tests than the three named.
|
|
- A test other than these fails after the edits. Do not change a test.
|