SECURITY: symlinks anywhere in the Syncthing-shared brain dir are followed by search, vectorstore, context and graph readers #157
Closed
opened 2026-10-03 14:57:13 +00:00 by mathias
·
26 comments
No Branch/Tag Specified
main
fix/pipeline-writes-158
fix/edit-path-io-192
fix/create-regular-188
fix/guard-special-179
fix/linkcheck-keeps-write-178
fix/write-dir-redirect-173
fix/tunnel-detect-174
fix/pipeline-root-157
w3/154-mcp-auth-wiring
fix/tunnel-traversal
test/in-brain-targets-171
fix/brain-scan-root-157
fix/graph-wiki-root-157
fix/vectorstore-root-157
fix/notes-root-157
fix/search-root-157
fix/brainwide-root-157
fix/raw-symlinks-153
feat/symlink-tripwire-157
fix/rest-auth-149
fix/123-brain-edit-envelope-check
feat/106-page-aware-extraction
fix/105-source-drift-on-main
fix/104-surface-merge-conflicts-on-main
agent/b1-compile-on-ingest
agent/104-surface-merge-conflicts
agent/105-source-drift
agent/107-loud-unconfigured-alerting
feat/brain-read-edit
fix/capture-i1-refusal-names-target
renovate/configure
feat/consolidate-single-harness
icebox/cmd-routing
feat/brain-pending-promote
fix/close-session-classification-refresh
fix/capture-summarywriter
v0.24.0
v0.23.1
v0.23.0
v0.22.0
v0.21.1
v0.21.0
v0.20.1
v0.20.0
v0.19.1
v0.19.0
v0.18.4
v0.18.3
v0.18.2
v0.18.1
v0.18.0
v0.17.0
v0.16.1
v0.16.0
v0.15.0
v0.14.0
v0.13.2
v0.13.1
v0.13.0
v0.12.1
v0.12.0
v0.11.2
icebox/cmd-routing-2026-07-01
v0.11.1
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#157
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by the PR #156 review, 2026-10-03. Out of #153's raw/ scope.
/var/lib/supervisor/brainis a Syncthingsendreceivefolder shared with flamingo, and Syncthing syncs symlinks..stignorelists only sessions, training-data, .git and log.md. So whoever controls a Syncthing peer can plantwiki/<wing>/<hall>/leak.md -> /any/pod-readable/file.search.Query(brain_search / brain_query) returned the target's contents in the excerpt (sentinel count 1).vectorstore/sync.go(embeds it),search.hydrate,mcp/tools_context.go readExcerpt,graphsync,frontmatterlint(--fixwrites through symlinks) all follow such a symlink.Impact: a file outside the brain gets indexed and returned to any MCP/REST caller. Since #149 that means token holders, plus whoever controls flamingo or koala.
Options (decision needed)
os.Rooton the brain dir and skips non-regular files (the #153 pattern, brain-wide). Thorough, but touches many packages.Recommended: 2 now as a tripwire, then 1.
Created via git-mcp on behalf of @mathias
Decision (Mathias, 2026-10-03): option 2 now as a tripwire, then option 1 (brain-wide readers through os.Root, skipping non-regular files). Option 3 (Syncthing) is not pursued.
Created via git-mcp on behalf of @mathias
pr-gatekeeper verdict: BLOCK (PR #159 @
796d226)Not merged. CI run 8893 is green,
CI=1 task checkEXIT=0 locally, and neither covers any of the findings below.Findings (most severe first)
cmd/server/symlink_check.go:16-18: the tripwire fails open on any scan error, not only a missing brain dir.filepath.WalkDiraborts on the first error andScanthrows away everything it had already found, so a single unreadable directory turns off the startup refusal.Repro (executed): brain with
planted -> /tmp/xplus an empty diraaa/at mode 000.startupSymlinkCheckreturnserr=<nil>. In the same tree,search.Querystill returns the sentinel from a symlinkedwiki/w/facts/leak.md(hits=1), because search logs walk errors and carries on (search.go:128-131).This is live-relevant: the pod runs as uid 1000 (
runAsUser: 1000, no root DAC override), and Syncthing on koala also runs as uid 1000, so a dir a peer syncs with mode 000 is unreadable to the pod. I did not test this against a real Syncthing peer. Even with no attacker, Syncthing deleting a dir in the middle of a walk produces the same silent pass.The fix is a correction, not a judgement call: return nil only when the root does not exist (
errors.Is(err, fs.ErrNotExist)fromEvalSymlinks), refuse on every other error, or make Scan record per-entry errors and keep walking.internal/symlinkguard/symlinkguard.go:68-70: Watch goes permanently dark on a persistent scan error. Each tick logsslog.Warnand skips the notifier, so the same unreadable dir silences the runtime alert too. Nothing pages anyone. Because this control's whole value is that someone finds out, a scan that fails is itself something to alert on.symlinkguard.go:75-84: a failed alert is never retried.last = keyis set beforeNotify, so if ntfy is down at that moment the alert is lost until the symlink set changes. Executed: a failing notifier was called 1 time over ~30 ticks with the symlink still in place. Only movelastforward after Notify succeeds (or n is nil).symlinkguard.go:41:.gitis skipped at every depth, not only at the root. Executed:wiki/w/facts/.git/leak.md -> outsidegives Scan found=0, while search sentinel hits=1. Syncthing is probably not a route for this, since the live.stignoreentry.git(no leading slash) matches at any depth. That leaves it reachable only by a writer local to koala. The real problem is that the PR's stated guarantee ("refuses to start while any symlink exists") is false. Skip onlyfilepath.Join(root, ".git"). I flipped it to root-only and the suite stayed green, so the fix costs nothing.cmd/server/main.go:369:BRAIN_SYMLINK_SCAN_INTERVAL<=0panics.time.NewTickerpanics with "non-positive interval for NewTicker" (executed), and because it happens in a goroutine it takes the whole server down into a crashloop. Clamp the value or reject it at startup.Minor: the dedupe key
strings.Join(found, "\n")collides for names that contain a newline ({"a\nb"}vs{"a","b"}).alertingWarningsdoesn't mention that the tripwire alert goes dark whenBRAIN_NTFY_URLis unset (it is set live, so there's no effect today).Risk label: issue #157 is
risk/high, PR isrisk/medium. A security control on the brain should carry the issue's label.Rule 9 (test names)
TestScanFollowsTheRootButNotEntries: the test has no symlinked entries, so "ButNotEntries" is never asserted. Rename it (e.g....ResolvesASymlinkedRoot) or add an entry symlink.TestScanFindsSymlinksAtAnyDepthAndSkipsGit: "any depth" is contradicted by finding 4, and "SkipsGit" passes for a skip at any depth as well as a root-only skip.TestWatchNotifiesOncePerNewSymlinkSet: uses only one set. Movinglast=after the empty-check, which suppresses the alert when a set disappears and reappears, stayed green.Flip tests
.gitskip.gitskip root-onlystartupSymlinkCheckandgo Watchfrom main.goWhat holds
context.Background()is never cancelled, but it is one goroutine for the life of the process, so there is no leak.INGEST_BRAIN_DIR=/app/brainis a hostPath of/var/lib/supervisor/brain, which has 0 symlinks, 0 find errors, 0 nested.git, and 0 dirs unreadable by uid 1000 (read-only check, 2026-10-03).Not checked
search.Query, which I did not execute (vectorstore, context, graph).pr-gatekeeper verdict, round 2: BLOCK (PR #159 @
956ad8b)Not merged. CI run 8895 is green and
CI=1 task checkEXIT=0.-race -count=5is also green. All of the round-1 findings are fixed, and I verified each one by running it. One fail-open remains, and it is the iron-law pattern itself.Finding (blocking)
ingestion/internal/symlinkguard/symlinkguard.go:74:_ = filepath.WalkDir(...)throws away the error when the brain root can't be listed, and Scan reports Clean. WhenReadDirfails on the root, the callback returns that error (if p == root { return err }). WalkDir passes it back up, and_ =drops it, so Scan returns an empty Result with a nil error.Repro (executed at
956ad8b):wiki/w/facts/leak.md -> outside.startupSymlinkCheckreturnserr=<nil>, andsearch.Queryreturns the sentinel (hits=1).knowledge/wikiand walks from there.err=<nil>), though readers then see nothing.Reachability: I don't think a Syncthing peer can change the folder root's mode (I have not verified that against Syncthing), so this most likely needs a writer local to koala. It is still a control reporting "clean" for a tree it never read. The fix is one line: return the WalkDir error from Scan, and let startup refuse on anything that isn't
ErrNotExist.Flipping
startupSymlinkCheckto pass on any scan error stays green. Today the only error Scan can return is ErrNotExist, so the refusal branch is never exercised. The fix above plus a test with the root at 0100 closes both gaps.Non-blocking
reported = ""on a clean scan stays green....AgainForANewSetis honestly named (it tests a different set), so this is a gap, not a rule-9 issue. In round 1 I confirmed by running it that the behaviour works.risk/highandrisk/mediumlabels are on the PR (gitea-mcp#61).Round-1 fixes: re-verified by execution
.gitwas skipped at every depthwiki/w/facts/.git/leak.md.gitis still skipped."wiki/a\nb.md"Flip tests
.gitskipped at any depthDeploy safety (read-only, re-checked now)
INGEST_BRAIN_DIR=/app/brainis a hostPath mount of/var/lib/supervisor/brain.Not checked
pr-gatekeeper verdict, round 3: PASS (PR #159 @
cda18e6). Not merged.This is a verdict only. The PR is
risk/high, so the merge is Mathias's call (CD deploys on merge).Blocking finding from round 2: closed, verified by execution
Scannow returns the root's WalkDir error. Deeper errors are still recorded as Unreadable. Startup refuses on any error except ErrNotExist.cda18e6, uid 1000)wiki/w/facts/leak.md(my round-2 repro; search would still serve it, hits=1)wiki/at mode 0100 (can be entered, not listed).gitFlip tests: all red
TestScanOfAnUnlistableRootIsAnError,TestStartupSymlinkCheckRefusesAnUnlistableBrainRoot)TestWatchReportsASetAgainAfterItWasCleared).gitskipped at any depthThe round-2 flips (no unreadable recording, no retry, no interval clamp) were red at
956ad8b. The code they cover hasn't changed since.Rule 9: I read each new test name against its assertions, and they match.
The
Geteuid()==0skips do not hide the tests in CI. The gitea-runner pod runs as uid 1000 withrunAsNonRoot: true, so the permission tests actually run there.CI and gate
cda18e6: success.CI=1 task check: EXIT=0, 0 FAILs.-race -count=3on both packages: green.Deploy safety (read-only, re-checked now)
INGEST_BRAIN_DIR=/app/brainis a hostPath mount of/var/lib/supervisor/brain. The pod runs as uid 1000.Residual, accepted or tracked
risk/mediumlabel is still on the PR (gitea-mcp#61).Not checked
Tripwire half (option 2) shipped and verified live. PR #159 squash-merged as
cad389fafter 3 adversarial review rounds; Mathias approved the merge. CD rolled outingestion:cad389f, the pod started (startup check passed: 0 symlinks, 0 unreadable dirs), the tripwire logged nothing, and the service answers.What runs now:
symlinkguard.Watchrescans everyBRAIN_SYMLINK_SCAN_INTERVALseconds (default 300) and sends one ntfy alert per new finding, retried until it sends.Still open here: option 1, brain-wide readers through
os.Root(search, vectorstore, context, graph, frontmatterlint). Until then, a symlink planted between scans is live for up to 5 minutes; it's reported, not removed. The wiring test is tracked in #154.Created via git-mcp on behalf of @mathias
Decision (Mathias, 2026-10-03): both, auto-remove first.
os.Root, done incrementally as defense in depth. About 12 model-reachable readers: search, hydrate, vectorstore sync, graphsync, the context excerpt, ReadNote/brain_get, the wiki inventory, wing indexes, the ingest merge's reviseOrCreate, and the provenance re-stamp.Created via git-mcp on behalf of @mathias
pr-gatekeeper verdict: BLOCK (PR #160 @
504feb3). Not merged.The removal itself is correct, and I verified that by execution (table below). Two things block. CI is red, and the alert path can be wedged by the very party it is meant to report. One claimed guard also has no assertion.
Blocking
CI run 8912 is red.
TestRawReadersLeakNothingUnderASymlinkSwapRace, from #156, not this PR.t.TempDircleanup failed with "directory not empty" because the test never waits for its swapper goroutine.CI=1 task checkwas EXIT=0, so this is a flake, but it is the estate's gate.symlinkguard.goWatch/pending: the alert queue is unbounded, and one rejected message wedges it permanently.break. A message the server will never accept therefore blocks every later alert forever, whilependingkeeps growing by one per removal event.symlinkguard.goremoveSymlinks: the "still a symlink" Lstat re-check has no test.fi == nilkeeps the suite green.os.RemoveAll(filepath.Join(resolved, rel))instead ofroot.Removealso keeps it green, so theos.Rootconfinement is unasserted too.removeSymlinkstest fixes both. It should pass a regular file, a path that goes through an in-root dir symlink, and a../path, and assert that each is reported failed and nothing was deleted.Verified by execution (holds)
../../..removeSymlinkscalled directly on a regular file, a path through an in-root dir link, a../escape, a missing pathSwap race (Lstat to Remove): I accept it.
os.Rootconfines removal to inside the brain, and the only file that can be hit is the one at the very path the peer just replaced. A sendreceive peer can already delete any brain file outright by syncing a deletion. The race gives a remote party nothing it doesn't already have.Non-blocking, for Mathias's attention
Titleheader is hard-coded to "brain-capture audit degraded", so symlink alerts arrive under that title (pre-existing inaudit/ntfy.go).TestWatchRemovesSymlinksButNeverTheirTargetsOrRegularFileschecks one outside file target. Dir targets and in-brain targets are not asserted. They hold by execution (above), but the name is broader than what the test proves.Flip tests
os.RemoveAlloutsideos.RootDeploy safety (read-only, re-checked)
/var/lib/supervisor/brain: 0 symlinks, 0 dirs unreadable by uid 1000, 0 find errors. Rollout removes nothing.BRAIN_SYMLINK_SCAN_INTERVALis set on the deployment, so the new 30 s default applies.Not checked
One consequence of the auto-remove decision that the record above didn't spell out (from the PR #160 review): the brain dir is a Syncthing sendreceive folder, so a symlink the tripwire removes on koala is also deleted on flamingo. Only the link is deleted, never its target. That follows from treating any symlink in the brain as an incident. If one were ever placed there on purpose on flamingo, it will disappear on both sides within one scan interval.
Created via git-mcp on behalf of @mathias
Gatekeeper review of PR #161 (slice 1, search, head
9c9ba31): BLOCK. Details are on the PR.Symlinks at every depth, symlinked wing and hall filters, and symlinked wiki/ and knowledge/ roots all returned 0 results. hydrate under a swap race leaked 0 times. Blocking: the type check on the opened handle in
brainfs.ReadRegularhas no test, and it is load-bearing. Without it, a swapped-in FIFO whose writer is held open hangs Query after 16 iterations, and the suite stays green. Query is about +10 ms per call at the live brain size.pr-gatekeeper verdict, round 2: BLOCK (PR #160 @
01de163). Not merged.Round-1 findings 1, 3 and 4 are closed, and I verified each by execution. CI run 8916 is green. The fix for finding 2 introduces a new silent loss: an ntfy outage longer than about 2.5 minutes loses alerts without any trace in the alert channel. That is the same class as #159 round-1 finding 3.
Blocking
symlinkguard.goalertQueue.flush: the drop-after-5-attempts rule can't tell a poison message from an outage, and drops are never counted.maxAttempts(5) ticks the head is dropped, then the next message, and so on.suppressed, so the recovery summary undercounts and says nothing about the drops.Repro (executed at
01de163):The Watch doc comment still says "an outage delays them but loses none", which is now false.
TestWatchDeliversEveryQueuedAlertAfterAnOutagepasses only because its outage is shorter than 5 ticks. Rule 9: the name claims "every" alert after "an outage", but the test only proves it for outages under 5 attempts.Fix (a correction):
suppressed.maxAttemptsticks that asserts the summary reports the dropped count.Optionally, drop only on a permanent 4xx and keep retrying on transport errors or 5xx. That would need Notify to expose the status code.
Verified by execution (holds)
TestRemoveSymlinksRefusesNonSymlinksEscapesAndPathsThroughLinkscovers a regular file, a path through an in-brain dir link, a../escape and a missing path-race -count=20on the api and watcher race tests, and-race -count=30on symlinkguard: all green. Swappers are joined in at.Cleanupthat runs before TempDir removal.CI=1 task checkEXIT=0, 0 FAILsOn the RemoveAll question: I agree that a dir swapped in after the Lstat can only be reached through a race, and that
root.Removerefuses a non-empty dir. The code comment is enough; I don't need a race test.Flip tests
Not checked
Gatekeeper round 2 on PR #161 (head
ffe46c7): BLOCK. Details are on the PR.The unit tests now pin readOpened, Rel and WalkRegular. Remaining: nothing checks that
ReadRegulargoes throughreadOpened. Bypassing it keeps the suite green and brings the held-writer FIFO hang back in Query (5 iterations). Fix: have the search FIFO swapper hold a writer open.pr-gatekeeper verdict, round 3: PASS (PR #160 @
4ba9d20). Not merged.This is a verdict only. The PR is risk/high and CD deploys on merge, so the merge is Mathias's call.
Round-2 blocker: closed, verified by execution
only.md)s.md) is deliveredAt the real ntfy limits, the capped alert (largest 2217 bytes in round 2) does not get a 413 at all.
Flip tests: all red
TestWatchDropsAPoisonMessageAndCountsIt)...AfterAShortOutage,...LosesNoAlertThroughALongOutage)TestNotifyClassifiesRejectionsAsPermanentOrNot)...QueueIsBoundedDuringAnOutage,...AccountsForEveryRemovalDuringAnOutage)Rule 9: I read the new and renamed test names against their assertions, and they match.
Gate and CI
4ba9d20: success.CI=1 task check: EXIT=0, 0 FAILs.-race -count=10on symlinkguard and audit: green.Deploy safety (read-only, re-checked)
/var/lib/supervisor/brain: 0 symlinks, 0 unreadable dirs, 0 find errors. Rollout removes nothing.Non-blocking, filed
#164: a hung ntfy delays removal.
Other open items: the ntfy title header predates this PR (the author will file it), and #157 records that deletions propagate to flamingo.
Not checked
Gatekeeper round 3 on PR #161 (slice 1, head
fd33e04): PASS. Evidence is on the PR. Not merged, verdict only.The readOpened bypass is now caught 10/10. No flakiness (140 runs, plus -race). No fd leak across tests, and the test is still effective at a 1024 fd limit.
Shipped and verified live (Mathias approved both merges):
ingestion:eb77305is rolled out and healthy, the tripwire logged nothing, and a livebrain_queryreturns results throughbrainfs.c986df6), 3 review rounds:eb77305), 3 review rounds: option 1 slice 1.internal/brainfspackage (os.Root, regular files only, non-blocking open plus handle check) is used bysearch.Queryandhydrate.Still open here: option 1, remaining slices (vectorstore sync, the context excerpt, ReadNote/brain_get, graphsync, the wiki inventory, wing indexes, the ingest merge's reviseOrCreate, and the provenance re-stamp). Budget about +10 ms per full brain walk. Related: #164 (a hung ntfy can stall removal to about 90 s), #163 (ntfy title), #154 (wiring test), #158 (writes under raw/).
Created via git-mcp on behalf of @mathias
Gatekeeper review of PR #165 (slice 2: readExcerpt, ReadNote, ReadScoped; head
b6f7dac): BLOCK. Details are on the PR.Confinement held: every reader flip goes red, and there is no flakiness at -count=10. Blocking:
(1)
ReadRegularMaxchecks the size on the handle but then reads without a bound. A file growing in place returned 100 MiB against a 1 MiB limit. Dropping the limit fromreadContainedalso stays green.(2) Rule 9: the three tests are named
...IsConfinedTo..., a goal rather than a mechanism, and the ReadNote test has no positive control.The brain_edit write path belongs with #158.
Gatekeeper round 2 on PR #165 (slice 2, head
02cecb2): PASS. Evidence is on the PR. Not merged, verdict only.A file growing during the read now returns 0 bytes over the limit (round 1: 100 MiB). Dropping the limit at readContained goes red. The test names are honest. CI run 9017 is green.
Slice 2 shipped, v0.21.1 (PR #165, squash
27a01dd). readExcerpt (brain_context), ReadNote (brain_get) and ReadScoped (brain_read) now read through brainfs.ReadRegularMaxbounds the read itself withLimitReader(max+1). Review round 1 BLOCKED on an unbounded read past the stat check; round 2 PASS.ingestion:27a01dd. Live smoke test: brain_context, brain_get and brain_read all returnwiki/entities/hyperguild.md.Slice 3 is in review: PR #166 (vectorstore.Sync via brainfs). Symlinked notes were embedded into pgvector. Their chunks are purged on the first sync after the deploy.
Remaining readers (from a grep of
os.ReadFile/WalkDiron main):Non-blocking from review:
brainfs.wrapAfterStatis a package-global test seam, so brainfs tests must not uset.Parallel. I'll add a comment in the next brainfs touch.Created via git-mcp on behalf of @mathias
Slice 3 merged (PR #166, squash
57d0fb0). vectorstore.Sync now goes through brainfs. Adversarial review rounds: BLOCKED, BLOCKED, PASS. The full reviews are on the PR (review ids 1–3).fs.WalkDirstats its root with a follow, and os.Root allows in-brain symlinks. Soknowledge -> .aliasedraw/,sessions/and root files into the embeddings, which main had not done. Fix: WalkRegular Lstat-checks its root, and Sync aborts before the delete pass on any walk error other than a missing scan root.<wing>. Fix:realDirchecks every prefix. This also closes the same gap in search's walk from slice 1.Accepted residual (Mathias, 2026-10-04): the directory-swap race is tracked in #167, with resolution triggers. A directory swapped for an in-brain symlink between the walk's listing and the read is still followed. The WalkRegular doc states this. The other accepted residual is hard links.
Non-blocking items left:
ErrNotDirsentinel would be clearer.I'll pick both up the next time I touch brainfs.
Next slices:
Created via git-mcp on behalf of @mathias
Gatekeeper review of PR #168 (slice 4: graphsync, wiki inventory/index; head
c7a6429): BLOCK. Details are in review id 4 on the PR.The static cases hold: outside links, in-brain file links, FIFOs, sockets, symlinked roots and type dirs, and a symlinked brainDir. CI run 9176 is green. Blocking:
os.SameFilecheck took all three to 0 leaks, and would also cover slices 1–3. The alternative is that Mathias extends #167 to cover file swaps and the tests are renamed....SkipsASymlinkedTypeDirand...SkipsSymlinkedNotesAndASymlinkedRoot. The code aborts in both cases; the first test discards the error, and silently skipping stays green.Non-blocking: outside content still reaches the LLM prompts through
readSourcePageandloadSchema(pipeline slice). LoadInventory on a missing brainDir now returns an error.Gatekeeper round 2 on PR #168 (slice 4, head
d90ce66): PASS. Evidence is in review id 5 on the PR. Not merged, verdict only.All three round-1 blockers are closed, and I verified each one by execution:
os.SameFileinreadOpenedMaxclosed them.CI run 9180 is green,
CI=1 task checkexits 0, and-race -count=5is green.Follow-up worth filing: the merged slice 1–3 callers have no per-caller in-brain case. Bypassing ReadRegular at search Query, hydrate, vectorstore Sync, the context excerpt or ReadNote stays green.
Slice 4 merged and live, v0.23.0 (PR #168 →
77e32de). Covers graphsync and wiki inventory/index, plusReadRegularSameFile, which covers every reader. Pod runsingestion:77e32de. The graph backfill indexed 1584 notes with no abort, and the embed sync added 16 with 0 errors. brain_query and brain_get work live. Follow-up: #171 (per-caller in-brain tests for slices 1–3).Slice 5 scope, confirmed by the PR #170 review:
DetectTunnels/readTitleAndCreated, which uses a blockingos.Openand hangs any brain_write/brain_update of a wiki note when a FIFO exists anywhere underwiki/; this is already true on mainfrontmatter.Scanbacks only thefrontmatterlintCLI, not the server, so it stays out of scope.Slice 6: pipeline (readSourcePage and loadSchema reach the LLM prompt: the PR #168 review got 10 outside hits), plus BackfillRefs, provenance and classification.
Created via git-mcp on behalf of @mathias
Gatekeeper review of PR #175 (slice 5a: wing indexes and linkcheck entryIndex through brainfs; head
6e3c51c): BLOCKED. Details are in review id 6 on the PR. Not merged, verdict only.entryIndexnow returns an error when a top-level linked tree (knowledge/orwiki/) is not a real directory: a symlink, in-brain or dangling, or a regular file.StripUnresolvedLinksruns first on every brain_write and brain_update, so every write is refused and the learning is lost. Main skipped such a tree and the write landed. I verified it against both main and the head throughbrainstore.Write. Fix: skip such a tree with a warning, and add a write-lands test._index.mdis written at all. IfReadDirDirslists symlinks, one symlinked hall stops the whole wing being indexed and one symlinked wing abortsBuildAllWingIndexes, and every brain test stays green.Other flips (main readers, ReadRegular Lstat/SameFile, ReadDirRegular, the error swallow) all go red. CI run 9311 is green,
CI=1 task checkexits 0, and-race -count=5is green. Green covers neither finding.Still open for slice 5b: brain_write still hangs on a FIFO under
wiki/throughAutoTunnel→readTitleAndCreated(verified).Gatekeeper round 2 on PR #175 (slice 5a, head
8fdf728): PASS. Evidence is in review id 7 on the PR. Not merged, verdict only (risk/high, Mathias merges).knowledge/, verified throughbrainstore.Write.ReadDirDirslists symlinks.CI=1 task checkexits 0, and-race -count=5is green.Non-blocking, to track: (a) the skip test covers only
knowledge/, notwiki/; (b) on main and here alike, an unreadable subdirectory underknowledge/orwiki/makesStripUnresolvedLinksabort and refuses every brain_write. That is a lost-learning path, not a regression, and wants its own issue. Slice 5b (FIFO hang via DetectTunnels) is still open.Gatekeeper review of PR #176 (slice 6: pipeline readers and classification.Load through brainfs; head
70df6db): BLOCKED. Details are in review id 8 on the PR. Not merged, verdict only.Blocking:
pipeline/backfill.go:82: the BackfillRefs concept-page read has no test. Reverting it toos.ReadFilekeeps the suite green, becauseLoadInventoryfilters a static symlinked page before the changed line runs. The read is reachable by a swap race. A scratch probe gets 0 leaks on head, and with the flip it copies other-brain content into the concept page in 3 of 3 runs. Fix: a per-caller swap test (#171 pattern).Held, verified by execution: no LLM-controlled page path escapes
wiki/(80 hostile type × title combos plus hostile sources;validPageTypeandwiki.Slug); every other reader flip goes red; classification.Load fails hard promptly on a FIFO, dir or dangling link, which is right because fail-open would drop explicit tags.Recorded for #158: the same probe shows BackfillRefs writing through a symlink swapped in between its read and its write (about 5k target overwrites in 3 s on head; already true on main). Also, symlinkguard does not report FIFOs, so a planted FIFO
classification.yamlpasses the startup tripwire and crash-loops at Load. On main it hung startup instead.CI run 9323 is green,
CI=1 task checkexits 0, and-race -count=5is green. Green covers none of finding 1.Slices 5a and 6 merged and live (PR #175 → ef39435; PR #176 →
eedb110).Check after #176:
Remaining reader work: slice 5b, DetectTunnels (PR #186, fixes #174), is in review. Once it merges, every brain reader in the server goes through brainfs.
frontmatter.Scanonly backs the CLI, so it stays out of scope.Related, in flight:
Open:
This issue closes when #186 merges.
Created via git-mcp on behalf of @mathias
Done. Every brain reader in the server now goes through brainfs. Slice 5b, DetectTunnels (PR #186, fixes #174), is merged and live: the pod restarted cleanly and the graph backfill indexed 1593 notes.
Related, merged:
Still open, each tracked:
frontmatter.Scanbacks only the CLI, so it is out of scope. Closing.Created via git-mcp on behalf of @mathias