feat(ingestion): the runtime symlink tripwire removes what it finds
CI / Lint / Test / Vet (pull_request) Failing after 33s
CI / Mirror to GitHub (pull_request) Skipped

Decision (#157): the tripwire only alerted, so a symlink planted in the
Syncthing-shared brain dir stayed readable until a human removed it.
Watch now unlinks every symlink it finds through an os.Root on the brain
dir (re-checking it is still a symlink; the target is never touched),
and the default scan interval drops from 5 min to 30 s, so a planted
link is readable for at most one interval.

Each removal is an alert naming what was removed; a persistent finding
(a link it could not remove, an unreadable dir, a scan error) alerts
once per distinct state. Alerts queue until Notify succeeds, so an ntfy
outage delays them without losing them, even though the next scan of
the cleaned brain is clean.

The startup check still refuses to start on any symlink.

Refs #157

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 18:16:43 +02:00
co-authored by Claude Opus 5.5
parent 687afa0145
commit 504feb372f
2 changed files with 117 additions and 34 deletions
@@ -182,8 +182,47 @@ func TestWatchReportsASetAgainAfterItWasCleared(t *testing.T) {
go Watch(ctx, brain, 10*time.Millisecond, n)
require.NoError(t, os.Symlink(t.TempDir(), link))
require.Eventually(t, func() bool { return n.count() == 1 }, 2*time.Second, 10*time.Millisecond)
require.NoError(t, os.Remove(link))
time.Sleep(60 * time.Millisecond)
// The tripwire removed it; planting it again is a new event.
require.Eventually(t, func() bool { _, err := os.Lstat(link); return os.IsNotExist(err) }, 2*time.Second, 10*time.Millisecond)
require.NoError(t, os.Symlink(t.TempDir(), link))
require.Eventually(t, func() bool { return n.count() == 2 }, 2*time.Second, 10*time.Millisecond)
}
// hyperguild#157 decision: the runtime tripwire removes a symlink it finds
// (the link, never its target), so it is readable for at most one interval
// instead of until a human acts, and the alert says what was removed.
func TestWatchRemovesSymlinksButNeverTheirTargetsOrRegularFiles(t *testing.T) {
brain := t.TempDir()
outside := filepath.Join(t.TempDir(), "target.md")
require.NoError(t, os.WriteFile(outside, []byte("keep me"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki"), 0o755))
link := filepath.Join(brain, "wiki", "leak.md")
require.NoError(t, os.Symlink(outside, link))
regular := filepath.Join(brain, "wiki", "note.md")
require.NoError(t, os.WriteFile(regular, []byte("note"), 0o644))
n := &recNotifier{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go Watch(ctx, brain, 10*time.Millisecond, n)
require.Eventually(t, func() bool {
_, err := os.Lstat(link)
return os.IsNotExist(err)
}, 2*time.Second, 10*time.Millisecond, "the symlink is removed")
require.Eventually(t, func() bool { return n.count() >= 1 }, 2*time.Second, 10*time.Millisecond)
b, err := os.ReadFile(outside)
require.NoError(t, err)
assert.Equal(t, "keep me", string(b), "the target is untouched")
b, err = os.ReadFile(regular)
require.NoError(t, err)
assert.Equal(t, "note", string(b), "regular files are untouched")
n.mu.Lock()
defer n.mu.Unlock()
assert.Contains(t, n.msgs[0], "removed")
assert.Contains(t, n.msgs[0], "wiki/leak.md")
}
func TestDefaultIntervalIsThirtySeconds(t *testing.T) {
assert.Equal(t, 30*time.Second, DefaultInterval)
}