brain_read returns a note verbatim; brain_edit is str_replace with an
exactly-once match. Both operate on INGEST_BRAIN_DIR, which is the pod's
mount of /var/lib/supervisor/brain — the tree brain-sync commits from, so
an edit reaches gitea on the next cycle rather than landing somewhere the
sync cannot see it (brain#2).
An adversarial design review before implementation changed three things,
each of which would otherwise have shipped:
1. Governance files are NOT in the model-facing tool. The original plan
allowed root *.md so CLAUDE.md had an MCP path at all — the absence of
one is what pushed a session to edit via the gitea REST API on
2026-06-16 and wedge the sync. But brain content is ingested from the
outside world, reaches a model's context via brain_query, and that
model would hold the editor: one poisoned page could rewrite the
CLAUDE.md a .dispatch-allow agent later loads as instructions. Worse,
brain-sync merges with --strategy-option=ours, so the live tree wins
against a human's corrective push on exactly the conflicting lines.
CLAUDE.md now goes through brain_edit_root: de-surfaced from
tools/list (the brain_index precedent, ADR-0012 rule 4) and gated on
the local static principal, so a claude.ai-origin session is refused.
The two scopes are disjoint — the privileged door has the smaller reach.
2. Reads needed the same allowlist, and brain_get already lacked it.
api.ReadNote resolves any brain-relative path and parseFrontmatter
returns the whole file when there is no fence, so brain_get reads
verbatim session transcripts under archive/ (up to 500 KB, never
BM25-indexed, so nobody thinks of them as reachable). A test proves
the exposure was live; the same Resolve now gates brain_get.
3. Canonicalise before allowlisting. A prefix check on the raw argument
accepts knowledge/../.git/hooks/post-commit, which Clean collapses to
a git hook that brain-sync executes inside a pod holding the Gitea
push credentials. Traversal, symlink escape, dotfiles, .git/, archive/,
sessions/ and raw/ are refused by a static backstop that does not
depend on the allowlist being right. There were no traversal tests in
this repo before.
Also from the review: overlapping match counting (bytes.Count says "aa"
occurs once in "aaa", so a Count-based uniqueness check accepts an
ambiguous edit); UTF-8 validation on both sides, because encoding/json
substitutes U+FFFD and would hand back bytes the file does not contain,
making every derived old_str unmatchable; refusal while .git/MERGE_HEAD
or index.lock exists, since an edit racing brain-sync's merge is either
hard-reset away or committed as a wrongly resolved conflict; flock plus a
per-path mutex with the read taken inside the lock, because two pods
share the hostPath during a RollingUpdate and uniqueness evaluated
against a stale read loses an update while reporting success; same-dir
*.tmp + rename + chmod 0644 + fsync of file and parent; and reindex only
when the edit touches frontmatter, since BM25 walks live and embeddings
are mtime-driven.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
Wires the #45 verbs into the MCP surface (all three sites: tools()
descriptors, handleCall dispatch, package doc comment).
- brain_update: supersede-by-slug or full path; rebuilds wing _index and
re-tunnels cross-wing matches against the new body (idempotent,
best-effort), re-indexes the graph, returns {id, path, content_hash,
superseded}.
- brain_get: fetch by id or path (both are the brain-relative handle);
returns {id, path, content_hash, frontmatter, body}.
- brain_write: return contract extended from {path} to {id, path,
content_hash} — path kept for backward compat — so the create path
also yields a stable handle.
id == relPath; content_hash == sha256 of the file bytes. Tests cover the
supersede happy path, missing-target error + no-create, get by id/path,
write handle, and an end-to-end re-embed test that drives the real
vectorstore.Sync re-index after an update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>