Conflict in ingestion/internal/brainfs/brainfs_test.go only: both sides
appended a test after TestReadRegularNeverReturnsAnInBrainFileSwappedInBehindASymlink
(this branch: TestWriteRegularRefusesFinalSymlinksFIFOsEscapesAndMissingFilesAndRewritesRegularFiles;
main: TestReadDirDirsListsOnlyRealSubdirsAndRefusesSymlinkedComponents).
Kept both, unchanged. No production code touched by the resolution.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BuildWingIndex/BuildAllWingIndexes list wings and halls with the new brainfs.ReadDirDirs and ReadDirRegular and read titles with ReadRegular: a symlinked note no longer reaches _index.md, a symlinked wing is refused (nothing written into its target), a symlinked hall is skipped. linkcheck entryIndex walks with WalkRegular, so a symlinked .md cannot make a link resolve, and a linked tree that is not a real directory is skipped with a warning (brain_write still lands).
Refs #157. Adversarial review: BLOCKED, PASS. Follow-up: #178.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
brain_tunnel passes caller-supplied source and target paths to
WriteTunnel, which checked only the first segments ("wiki/<wing>/...")
and joined them onto the brain dir. "wiki/b/x/../../../../tmp/f.md"
resolved outside the brain, and appendSeeAlso read and rewrote that file
(verified by execution): any MCP token holder could append to any
existing file the pod user can write, inside the brain (raw/, sessions/,
classification.yaml) or outside it.
- checkNotePath: only a clean, local wiki/.../<slug>.md path, with no
".", ".." or empty segments.
- Both notes are read and rewritten through brainfs. New
brainfs.WriteRegular: never creates; Lstat through the root, open with
O_NOFOLLOW|O_NONBLOCK, and require the opened handle to be the same
regular file (os.SameFile). os.Root.OpenFile follows an in-root final
symlink even with O_NOFOLLOW (verified on go1.27), so the flag alone
does not refuse it.
- The source is no longer rewritten when the target is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graphsync, wiki inventory/index read through brainfs; brainfs refuses an in-brain file swapped in behind a symlink (os.SameFile). Gatekeeper round 2 PASS on d90ce66, CI run 9180 green.
Refs #157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
vectorstore.Sync walks and reads via brainfs (regular files only, handle-checked), so symlinked notes are no longer embedded and their old chunks are purged. brainfs.WalkRegular requires every component of its root to be a real directory (fixes in-brain root aliasing in Sync and search Wing/Wing+Hall). Walk errors, including a symlinked scan root, abort Sync before the delete pass. brainfs gains Lstat. Directory-swap race recorded as accepted residual (#167).
Refs #157. Adversarial review: BLOCKED, BLOCKED, PASS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.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>