M5a plan: task 16, the gate passes on macOS; the review's tasks become 17 to 20
The work moved to titan (macOS), where the gate failed for reasons older than M5a: sockets under macOS's long TMPDIR pass its 104-byte limit, gatewayd reads /proc/self for its uid, and /tmp is /private/tmp. Task 16 sets TMPDIR=/tmp for every process Cargo runs (.cargo/config.toml [env]), has gatewayd take its uid from the owner of a pipe it makes (no dependency, no unsafe), and hands over a toolkit shell.rs that compares resolved paths. On a scratch tree the gate passes on titan and on talos with 936 tests, and a wrong uid fails 7 gatewayd tests. Tasks 16 to 19 are renumbered 17 to 20. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# M5a task 19: 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.
|
||||
Reference in New Issue
Block a user