Review round 1 on PR #170 found two more handlers with the #169 class of
bug (verified by execution):
- brain_update resolved its caller path with resolveWithin, a lexical
containment check: any brain file could be superseded
(classification.yaml, raw/, sessions/), and a final symlink was
written through. UpdateNote now resolves like brain_edit
(notes.Resolve, ScopeNotes: knowledge/ and wiki/ .md notes), and reads
and rewrites through brainfs (ReadRegular, WriteRegular).
- session_log joined the caller's session_id onto the sessions dir and
opened it O_CREATE|O_APPEND: an id with path segments created or
appended a .jsonl file anywhere the pod could write. The id must now
be a plain name (^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$, which every live
session file matches), and the open adds O_NOFOLLOW|O_NONBLOCK. Read
checks the id too.
- Tunnel tests: the .txt case now names an existing file (it failed on
"missing" before), the symlink test adds an in-brain target, and the
FIFO comment no longer claims the handle check is the only guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readExcerpt, ReadNote and ReadScoped now read through brainfs (os.Root, regular files only, type checked on the opened handle), so a symlink swapped in after a check is not followed and a FIFO does not hang the reader. ReadRegularMax bounds the read itself with io.LimitReader(max+1). notes.Target carries the brain root.
Refs #157. Adversarial review round 2: PASS (round 1 BLOCKED: unbounded read, rule-9 test names).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
Closes#144 (audit line per tools/call) and #145 (origin/written_by stamped on every model-reachable write, surfaced in brain_query/brain_get).
Passed pr-gatekeeper after four rounds of adversarial review, verified by execution: #147
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
Closes#125. Skips updated_at/supersedes/supersede_reason in the caller fragment merge loop so they cannot be set via the fragment, only via the intended entry points.
delete_branch_after_merge=true
Co-authored-by: Mathias <mthbqv@gmail.com>
The BrainStore is not the only door. POST /write — the HTTP path CLAUDE.md
hands to curl and scripts — calls api.WriteNote directly, so the strip
shipped one commit ago covered brain_write and capture while leaving the raw
door ungated. A gate one door wide is not a gate.
The BrainStore keeps its own call so it can still report Ref.Unlinked to the
MCP caller; running the strip twice is a no-op on content the primitive has
already cleaned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the api-layer half of #45. UpdateNote supersedes a note in
place (whole-note body replace, frontmatter re-stamp: updated_at,
supersedes=prior content hash, supersede_reason), preserving created_at,
wing, hall, and any custom fields. Never creates — a missing target is
an error so callers fall back to brain_write. ReadNote is the read-after-
write primitive (frontmatter + body + content_hash). ContentHash is the
sha256 handle that round-trips write/update → get.
Frontmatter is edited via a line-preserving ordered editor rather than a
yaml.v3 round-trip, which would reorder keys and strip comments — the
brain writes flat key:value frontmatter by hand.
Embeddings are not refreshed here: the rewritten file's mtime advances,
which the out-of-band vectorstore.Sync ticker uses to re-embed it — the
same mechanism brain_write relies on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>