Review round 1 on PR #168:
- ReadRegular/ReadRegularMax Lstat the path, then open it again by path;
os.Root follows an in-root final symlink even with O_NOFOLLOW, so a
symlink to another brain file swapped in between was read (8k-109k
raw/ leaks in RebuildIndex, LoadInventory, IndexDoc). The opened handle
must now be the same file as the Lstat (os.SameFile). This covers every
ReadRegular caller, slices 1-3 included.
- Every symlink test pointed outside the brain, which os.Root refuses on
its own. Each now has an in-brain case too: a static in-brain link per
reader, AssertNoLeakFrom with an in-brain target, and brainfs unit tests
for ReadDirRegular and the in-brain swap.
- brainfstest: swap links are relative (os.Root refuses every absolute
link, so an absolute in-brain target tested nothing), and each swapper
has its own temp name (two swappers on one path deadlocked on each
other's FIFO).
- IndexDoc indexes only a clean wiki/ or knowledge/ path (callers pass
caller-supplied paths).
- Rule 9: TestLoadInventoryReturnsErrNotRegularForASymlinkedTypeDir now
asserts the error; the backfill test says it aborts on a symlinked root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graphsync.IndexDoc/BackfillFromBrainDir and wiki.LoadInventory/
RebuildIndex read notes by path (os.ReadFile, os.Open, os.ReadDir +
open). A symlinked note put the target's title, aliases and wikilinks
into the knowledge graph, the wiki inventory (and the LLM prompts built
from it) and wiki/index.md; a FIFO hung each read.
- graphsync: IndexDoc and the backfill read through brainfs; the
backfill walks with WalkRegular, skips a missing root and aborts on
any other walk error, including a symlinked root.
- wiki: LoadInventory lists type dirs with the new
brainfs.ReadDirRegular (every component a real dir, regular files
only) and reads frontmatter through the root; a symlinked type dir is
an error. RebuildIndex reads first sentences through the root.
- brainfstest's outside file now carries the sentinel in frontmatter
title and aliases, a heading and a wikilink, so extractors that ignore
the body are tested too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commit 2 of Track A. Service stays a no-op until BRAIN_GRAPH_ENABLED=
true; flipping it on creates the schema (idempotent), starts indexing
every successful write, and optionally backfills the existing brain
dir.
- internal/graphsync: best-effort wrapper around graph.Extract +
graphstore. IndexDoc reads docPath under brainDir, parses, upserts
entity + replaces edges. BackfillFromBrainDir walks wiki/ +
knowledge/. Both are no-ops on nil store so callers wire
unconditionally.
- mcp.Server gains WithGraph builder + graphsync.Store field.
brain_write, brain_ingest, brain_ingest_raw, brain_tunnel call
indexInGraph after success — failures slog.Warn but never
propagate (graph is augmentation, not correctness).
- cmd/server gates the wiring on BRAIN_GRAPH_ENABLED=true (default
off so first rollout doesn't surprise). BRAIN_GRAPH_BACKFILL=true
triggers a one-shot walk of the brain dir on boot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>