The remaining writers created or rewrote brain files by path:
- pipeline buildAndWrite: os.MkdirAll + os.WriteFile of every page.
- BackfillRefs: os.WriteFile of each concept page; a page swapped for a
symlink after the read was written through (the race test overwrote an
in-brain target before this change).
- watcher: the raw/processed and raw/failed archive copies (os.Create),
the .processed/.failed markers and the log.md entry (os.OpenFile).
- cmd/server: the claude-session archive.
All now go through brainfs PutRegular/AppendRegular (new package-level
PutFile/AppendFile for one-off writers: the watcher's call-site lint bans
.Open calls in that file so raw/ reads stay on OpenRegular).
Stacked on #189 (needs PutRegular/AppendRegular).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writers that created or rewrote brain files by path followed symlinks:
- promote/reject: os.MkdirAll + os.WriteFile of the destination, then
os.Remove of the raw source. Now PutRegular + brainfs.Remove; the source
is kept when the write is refused.
- StampProvenanceFile (brain_edit's re-stamp): provenance.ApplyFile read
and rewrote by path. Now ReadRegular + WriteRegular; ApplyFile, now
unused, is removed.
- BuildWingIndex (_index.md) and wiki.RebuildIndex (wiki/index.md): now
PutRegular.
- wiki.AppendLog (log.md) and AutoTunnel's fuzzy-candidate log
(raw/tunnel-candidates-*.md): now AppendRegular; tunnel's fileExists,
now unused, is removed.
- New brainfs.Remove: clean path, real parent, removes a final symlink
itself, never its target.
brain_edit's own atomic rename was already refused behind a symlinked
directory by #187's read check; a guard test pins it.
Pipeline writes, BackfillRefs, the watcher's raw/ markers and the session
archive in cmd/server follow in a separate PR (#158).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brain_write created a new note with os.MkdirAll + os.WriteFile by path,
so a symlinked wing or hall directory redirected the note elsewhere in
the brain or outside it.
New brainfs primitives for creating writes:
- PutRegular: create or replace. Missing parents are created one
component at a time through the root and every existing component
must be a real directory; an existing file is replaced only via
WriteRegular's checks; a new one is created with O_EXCL and confirmed
(Lstat + SameFile) before writing.
- AppendRegular: create-or-append with the same parent rule, writing
only after confirming the path is the regular file it opened.
- RealParent: the clean-path + real-directory check, for writers that do
their own atomic rename.
writeHallNote now uses PutRegular.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadRegular, ReadRegularMax and WriteRegular require a clean, local path and a real directory at every level above the file, so an in-brain directory symlink (or a ".." after one) can no longer redirect brain_update, brain_tunnel, brain_read/get or the other readers elsewhere in the brain. session.Append opens through an os.Root on the brain dir, refuses a non-directory sessions dir, and writes only after confirming the log path is the regular file it opened.
Fixes#173. Adversarial review: BLOCKED, PASS. Follow-up: #188 (brain_write create path).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
brain_tunnel accepts only clean wiki/.../*.md paths and reads/rewrites both notes through brainfs; new brainfs.WriteRegular never creates, never writes through a final symlink (Lstat + O_NOFOLLOW|O_NONBLOCK + os.SameFile on the handle, since os.Root follows in-root final symlinks even with O_NOFOLLOW). brain_update resolves like brain_edit (knowledge/ and wiki/ notes only) and reads/rewrites through brainfs. session_log requires a plain-name session id and opens with O_NOFOLLOW|O_NONBLOCK.
Fixes#169. Adversarial review: BLOCKED, BLOCKED, PASS (round 3 by the estate session's reviewer). Follow-ups: #173, #174.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
AssertNoLeakFrom and the search Query race test passed vacuously when
every symlink swap failed or every read errored. Both now count
successful symlink swaps and reads that returned the inside note and
fail if either is zero; the Query race reader no longer swallows
Query's error.
Verified by flipping: a swapper whose os.Symlink always fails turns
notes, wiki and search race tests red; Query on a missing brain dir
turns the search race test red.
Closes#177
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>
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>