Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #159.
This commit is contained in:
@@ -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"
|
||||
@@ -173,7 +174,8 @@ func alertingWarnings(getenv func(string) string) []string {
|
||||
var out []string
|
||||
if getenv("BRAIN_NTFY_URL") == "" {
|
||||
out = append(out, "alerting DISABLED: BRAIN_NTFY_URL unset — "+
|
||||
"brain_edit_root governance rewrites and capture audit degradation will not notify")
|
||||
"brain_edit_root governance rewrites, capture audit degradation and the "+
|
||||
"brain symlink tripwire (hyperguild#157) will not notify")
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -239,6 +241,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 +366,8 @@ func main() {
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// hyperguild#157: report a symlink that appears in the brain while running.
|
||||
go symlinkguard.Watch(ctx, brainDir, symlinkScanInterval(os.Getenv), buildNotifier())
|
||||
|
||||
// Built before the watcher, the graph backfill and session ingestion, and
|
||||
// retried rather than fatal (#83). Two separate reasons:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/symlinkguard"
|
||||
)
|
||||
|
||||
// startupSymlinkCheck is the hyperguild#157 tripwire at boot: a symlink in the
|
||||
// brain dir would let brain readers serve a file from outside the brain, so
|
||||
// the server does not start until it is removed. A directory the scan cannot
|
||||
// read could hide one, so it refuses on that too. A missing brain dir (first
|
||||
// boot) is the only case it lets pass.
|
||||
func startupSymlinkCheck(brainDir string) error {
|
||||
r, err := symlinkguard.Scan(brainDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("refusing to start: cannot scan the brain dir %s (hyperguild#157): %w", brainDir, err)
|
||||
}
|
||||
if !r.Clean() {
|
||||
return fmt.Errorf("refusing to start: brain dir %s has %s (hyperguild#157); fix them first", brainDir, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// symlinkScanInterval reads BRAIN_SYMLINK_SCAN_INTERVAL (seconds), falling
|
||||
// back to the default on an unset, non-numeric or non-positive value.
|
||||
func symlinkScanInterval(getenv func(string) string) time.Duration {
|
||||
if s, err := strconv.Atoi(getenv("BRAIN_SYMLINK_SCAN_INTERVAL")); err == nil && s > 0 {
|
||||
return time.Duration(s) * time.Second
|
||||
}
|
||||
return symlinkguard.DefaultInterval
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// hyperguild#157: the server refuses to start while a symlink exists in the
|
||||
// brain dir, or while any part of it cannot be scanned (which could hide
|
||||
// one); a missing brain dir is the only thing it lets pass.
|
||||
func TestStartupSymlinkCheckRefusesABrainWithASymlink(t *testing.T) {
|
||||
brain := t.TempDir()
|
||||
require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(brain, "planted")))
|
||||
err := startupSymlinkCheck(brain)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "planted")
|
||||
}
|
||||
|
||||
func TestStartupSymlinkCheckRefusesABrainWithAnUnreadableDir(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("root reads mode-000 directories")
|
||||
}
|
||||
brain := t.TempDir()
|
||||
locked := filepath.Join(brain, "aaa")
|
||||
require.NoError(t, os.MkdirAll(locked, 0o755))
|
||||
require.NoError(t, os.Chmod(locked, 0o000))
|
||||
t.Cleanup(func() { _ = os.Chmod(locked, 0o755) })
|
||||
err := startupSymlinkCheck(brain)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "aaa")
|
||||
}
|
||||
|
||||
func TestStartupSymlinkCheckPassesACleanOrMissingBrain(t *testing.T) {
|
||||
brain := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(brain, "n.md"), []byte("x"), 0o644))
|
||||
assert.NoError(t, startupSymlinkCheck(brain))
|
||||
assert.NoError(t, startupSymlinkCheck(filepath.Join(brain, "missing")))
|
||||
}
|
||||
|
||||
func TestSymlinkScanIntervalFallsBackOnNonPositive(t *testing.T) {
|
||||
for _, v := range []string{"0", "-5", "", "junk"} {
|
||||
got := symlinkScanInterval(func(string) string { return v })
|
||||
assert.Positive(t, got, "env %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertingWarningsNamesTheSymlinkTripwire(t *testing.T) {
|
||||
w := strings.Join(alertingWarnings(func(string) string { return "" }), " ")
|
||||
assert.Contains(t, w, "symlink tripwire")
|
||||
}
|
||||
|
||||
func TestStartupSymlinkCheckRefusesAnUnlistableBrainRoot(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("root lists mode-0100 directories")
|
||||
}
|
||||
brain := t.TempDir()
|
||||
require.NoError(t, os.Chmod(brain, 0o100))
|
||||
t.Cleanup(func() { _ = os.Chmod(brain, 0o755) })
|
||||
assert.Error(t, startupSymlinkCheck(brain))
|
||||
}
|
||||
Reference in New Issue
Block a user