Files
sift/cmd/sift/serve.go
T
kyle 4372673184 p2.7: prometheus /metrics
Default GET /metrics is Prometheus text; JSON stays at /metrics.json
and ?format=json. :8443 needs the metrics caller bearer; loopback
stays open. Restricted callers have no per-caller series and no
tavily:restricted gauge.
2026-09-23 21:49:38 -07:00

258 lines
6.3 KiB
Go

package main
import (
"context"
"flag"
"log/slog"
"net"
"net/netip"
"os"
"os/signal"
"strings"
"syscall"
"git.wntrmute.dev/kyle/sift/internal/api"
"git.wntrmute.dev/kyle/sift/internal/auth"
"git.wntrmute.dev/kyle/sift/internal/config"
"git.wntrmute.dev/kyle/sift/internal/dcache"
"git.wntrmute.dev/kyle/sift/internal/digest"
"git.wntrmute.dev/kyle/sift/internal/fetch"
"git.wntrmute.dev/kyle/sift/internal/fleetctx"
"git.wntrmute.dev/kyle/sift/internal/guard"
"git.wntrmute.dev/kyle/sift/internal/search"
"git.wntrmute.dev/kyle/sift/internal/selfip"
)
type denyFlags []netip.Prefix
func (d *denyFlags) String() string {
var b []string
for _, p := range *d {
b = append(b, p.String())
}
return strings.Join(b, ",")
}
func (d *denyFlags) Set(s string) error {
p, err := netip.ParsePrefix(s)
if err != nil {
return err
}
*d = append(*d, p)
return nil
}
func defaultConfigPath() string {
if p := os.Getenv("SIFT_CONFIG"); p != "" {
return p
}
return "/etc/sift/config.toml"
}
func serveMain() {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
listen := fs.String("listen", "127.0.0.1:8890", "listen address")
configPath := fs.String("config", defaultConfigPath(), "config file")
keysPath := fs.String("keys", "/var/lib/sift/keys.toml", "caller keys file")
ua := fs.String("user-agent", "sift/"+version, "outbound User-Agent")
var deny denyFlags
fs.Var(&deny, "deny-prefix", "extra CIDR to block (repeatable)")
g := &guard.Guard{}
registerTestFlags(fs, g)
_ = fs.Parse(os.Args[1:])
log := slog.New(slog.NewTextHandler(os.Stdout, nil))
cfg, err := config.Load(*configPath)
if err != nil {
log.Error("config", "err", err, "path", *configPath)
os.Exit(1)
}
if *listen == "127.0.0.1:8890" && cfg.Listen != "" {
*listen = cfg.Listen
}
if *ua == "sift/"+version && cfg.UserAgent != "" {
*ua = cfg.UserAgent
}
for _, s := range cfg.DenyPrefixes {
p, err := netip.ParsePrefix(s)
if err != nil {
log.Error("deny_prefixes", "err", err, "value", s)
os.Exit(1)
}
deny = append(deny, p)
}
g.DenyPrefixes = deny
if n := len(deny); n > 0 {
log.Info("deny prefixes loaded", "count", n)
}
store, err := auth.Load(*keysPath)
if err != nil {
log.Error("keys", "err", err, "path", *keysPath)
os.Exit(1)
}
hasMetrics := false
for _, a := range store.Callers() {
if !a.MetricsOnly() {
continue
}
hasMetrics = true
if a.Workspace != "" {
log.Warn("metrics caller has workspace; ignored", "caller", a.Name)
}
}
if !hasMetrics {
log.Warn("no metrics caller in keys; :8443 /metrics will 401")
}
fetcher := &fetch.Fetcher{Guard: g, UA: *ua}
parent, stop := context.WithCancel(context.Background())
defer stop()
loadSecretEnv(secretEnvPath(), log)
srv := api.New(version, g, fetcher, store, log)
srv.PermitRestricted = cfg.Context.PermitRestricted
cachePath := cfg.Digest.Cache
if cachePath == "" {
cachePath = "/var/lib/sift/cache.db"
}
var cache *dcache.DB
if cfg.Dixie.Chat != "" || cfg.Search.AnyEnabled() {
c, err := dcache.Open(cachePath, 500<<20, log)
if err != nil {
log.Warn("cache", "err", err)
} else {
cache = c
}
}
if cfg.Dixie.Chat != "" {
srv.Digest = &digest.Engine{
ChatURL: cfg.Dixie.Chat,
EmbedURL: cfg.Dixie.Embed,
EmbedModel: cfg.Dixie.EmbedModel,
Model: cfg.Digest.Model,
Thinking: cfg.Digest.Thinking,
Gate: digest.NewGate(cfg.Dixie.MaxInflight, cfg.Dixie.QueueDepth, config.ParseDuration(cfg.Dixie.QueueWait, digest.DefaultQWait)),
Cache: cache,
Log: log,
}
}
var emb search.Embedder
if srv.Digest != nil {
emb = srv.Digest
}
srv.Search = search.Build(cfg.Search, cache, emb, log)
if cfg.Context.Honcho != "" {
allow := cfg.Context.Workspaces
if len(allow) == 0 {
allow = []string{"hermes-fleet"}
}
srv.Ctx = &fleetctx.Client{Base: cfg.Context.Honcho, Allow: allow, Permit: cfg.Context.PermitRestricted, Log: log}
var maps []fleetctx.CallerWS
for _, a := range store.Callers() {
maps = append(maps, fleetctx.CallerWS{Name: a.Name, Workspace: a.Workspace})
}
srv.Ctx.CheckMappings(maps)
}
if len(cfg.SelfIP.Sources) > 0 {
disc := &selfip.Discoverer{
Cfg: selfip.Config{
Sources: cfg.SelfIP.Sources,
Interval: config.ParseDuration(cfg.SelfIP.Interval, selfip.DefaultInterval),
KeepPrevious: config.ParseDuration(cfg.SelfIP.KeepPrevious, selfip.DefaultKeepPrevious),
Path: cfg.SelfIP.Path,
},
Guard: g,
Log: log,
Get: func(ctx context.Context, u string) ([]byte, error) {
res, err := fetcher.Fetch(ctx, u)
if err != nil {
return nil, err
}
return res.Body, nil
},
}
disc.Start(parent)
srv.SelfIP = disc
}
hs := srv.HTTPServer(*listen)
ln, err := net.Listen("tcp", *listen)
if err != nil {
log.Error("listen", "err", err)
os.Exit(1)
}
log.Info("sift listening", "addr", ln.Addr().String(), "version", version)
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP)
for range ch {
if err := store.Reload(); err != nil {
log.Error("keys reload", "err", err)
} else {
log.Info("keys reloaded")
if srv.Ctx != nil {
var maps []fleetctx.CallerWS
for _, a := range store.Callers() {
maps = append(maps, fleetctx.CallerWS{Name: a.Name, Workspace: a.Workspace})
}
srv.Ctx.CheckMappings(maps)
}
}
}
}()
errCh := make(chan error, 1)
go func() { errCh <- hs.Serve(ln) }()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
select {
case <-sig:
stop()
_ = hs.Close()
case err := <-errCh:
stop()
if err != nil {
log.Error("serve", "err", err)
os.Exit(1)
}
}
}
func secretEnvPath() string {
if p := os.Getenv("SIFT_SECRET"); p != "" {
return p
}
return "/var/lib/sift/secret.env"
}
// loadSecretEnv sets KEY=value from the on-box file when the process env
// does not already have the key. Never logs values.
func loadSecretEnv(path string, log *slog.Logger) {
b, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(b), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
v = strings.Trim(v, `"'`)
if k == "" || os.Getenv(k) != "" {
continue
}
_ = os.Setenv(k, v)
}
_ = log
}