feat(ingestion): symlink tripwire for the Syncthing-shared brain dir
CI / Lint / Test / Vet (pull_request) Successful in 34s
CI / Mirror to GitHub (pull_request) Skipped

The brain dir is a Syncthing sendreceive folder and Syncthing syncs
symlinks; search, vectorstore, context and graph readers follow one out
of the brain and would index or return what it points to (reproduced
with search.Query). Decision (#157): a tripwire now, brain-wide os.Root
readers next.

- symlinkguard.Scan lists every symlink inside the brain dir (skipping
  .git; the brain dir itself may be a mount symlink).
- At startup the server refuses to start while any exists, naming them.
- symlinkguard.Watch rescans every BRAIN_SYMLINK_SCAN_INTERVAL seconds
  (default 300) and sends one ntfy alert per new set of symlinks.

The live brain dir had 0 symlinks on 2026-10-03, checked before this
change, so deploying it does not trip.

Refs #157

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:04:41 +02:00
co-authored by Claude Opus 5.5
parent cfe3f24e25
commit 796d226497
5 changed files with 235 additions and 0 deletions
+7
View File
@@ -33,6 +33,7 @@ import (
"github.com/mathiasbq/hyperguild/ingestion/internal/reranker"
"github.com/mathiasbq/hyperguild/ingestion/internal/search"
"github.com/mathiasbq/hyperguild/ingestion/internal/startupretry"
"github.com/mathiasbq/hyperguild/ingestion/internal/symlinkguard"
"github.com/mathiasbq/hyperguild/ingestion/internal/vectorstore"
"github.com/mathiasbq/hyperguild/ingestion/internal/watcher"
"github.com/mathiasbq/hyperguild/ingestion/internal/webhook"
@@ -239,6 +240,10 @@ func main() {
}
brainDir := envOr("INGEST_BRAIN_DIR", "../brain")
if err := startupSymlinkCheck(brainDir); err != nil {
logger.Error("brain symlink tripwire", "err", err)
os.Exit(1)
}
port := envOr("INGEST_PORT", "3300")
llmURL := envOr("INGEST_LLM_URL", "http://iguana:4000/v1")
@@ -360,6 +365,8 @@ func main() {
}
ctx := context.Background()
// hyperguild#157: report a symlink that appears in the brain while running.
go symlinkguard.Watch(ctx, brainDir, time.Duration(envInt("BRAIN_SYMLINK_SCAN_INTERVAL", 300))*time.Second, buildNotifier())
// Built before the watcher, the graph backfill and session ingestion, and
// retried rather than fatal (#83). Two separate reasons: