// Package fleetctx is Honcho retrieval for sift. Read-only. // The restricted workspace (guardian-star) is reachable only when the // authenticated caller is in Client.Permit and the caller's mapped workspace // is that name. Workspace is never taken from the request. package fleetctx import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "net/http" "net/url" "regexp" "strings" "sync/atomic" "time" "unicode/utf8" "git.wntrmute.dev/kyle/sift/internal/auth" ) const ( Restricted = "guardian-star" Timeout = 3 * time.Second Limit = 5 MaxChars = 300 ) type Item struct { Peer string `json:"peer"` Content string `json:"content"` CreatedAt string `json:"created_at"` Session string `json:"session"` } type Client struct { Base string Allow []string Permit []string // caller names allowed to query Restricted HTTP *http.Client Log *slog.Logger Calls atomic.Uint64 Hits atomic.Uint64 Failures atomic.Uint64 } func restricted(ws string) bool { return strings.EqualFold(strings.TrimSpace(ws), Restricted) } func permitted(caller string, permit []string) bool { if caller == "" { return false } for _, p := range permit { if p == caller { return true } } return false } var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) func workspaceOK(ws string) bool { return workspaceName.MatchString(ws) } func allowed(ws string, allow []string) bool { for _, a := range allow { if a == ws { return true } } return false } // Search returns prior knowledge for workspace. Failure → (nil, warning). Never // an error that fails the fetch. // Restricted workspace requires caller ∈ Permit. A permitted caller whose mapped // workspace is not Restricted is refused and never queried (no fallback). func (c *Client) Search(ctx context.Context, workspace, caller, query string) (items []Item, warn string) { if c == nil || query == "" { return nil, "" } if permitted(caller, c.Permit) && !restricted(workspace) { if c.Log != nil { c.Log.Error("restricted caller mapped off guardian-star", "caller", caller, "workspace", workspace) } c.Failures.Add(1) return nil, "context refused: workspace" } if restricted(workspace) && !permitted(caller, c.Permit) { if c.Log != nil { c.Log.Warn("context refused", "workspace", workspace, "caller", caller) } c.Failures.Add(1) return nil, "context refused: workspace" } if !workspaceOK(workspace) { c.Failures.Add(1) return nil, "context skipped: workspace" } if !allowed(workspace, c.Allow) { if c.Log != nil { c.Log.Warn("context workspace not allowlisted", "workspace", workspace) } return nil, "context skipped: workspace" } c.Calls.Add(1) sctx, cancel := context.WithTimeout(ctx, Timeout) defer cancel() path := strings.TrimRight(c.Base, "/") + "/v3/workspaces/" + url.PathEscape(workspace) + "/search" payload, _ := json.Marshal(map[string]any{"query": query, "limit": Limit}) req, err := http.NewRequestWithContext(sctx, http.MethodPost, path, bytes.NewReader(payload)) if err != nil { c.Failures.Add(1) return nil, "context failed" } req.Header.Set("Content-Type", "application/json") httpClient := c.HTTP if httpClient == nil { httpClient = &http.Client{Timeout: Timeout} } resp, err := httpClient.Do(req) if err != nil { c.Failures.Add(1) return nil, "context failed" } defer resp.Body.Close() if resp.StatusCode != 200 { c.Failures.Add(1) return nil, "context failed" } var raw []struct { Content string `json:"content"` CreatedAt string `json:"created_at"` PeerID string `json:"peer_id"` SessionID string `json:"session_id"` WorkspaceID string `json:"workspace_id"` } if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { c.Failures.Add(1) return nil, "context failed" } for _, m := range raw { if m.WorkspaceID != workspace { continue } content := m.Content if utf8.RuneCountInString(content) > MaxChars { r := []rune(content) content = string(r[:MaxChars]) } items = append(items, Item{ Peer: m.PeerID, Content: content, CreatedAt: m.CreatedAt, Session: m.SessionID, }) } if len(items) > 0 { c.Hits.Add(1) } return items, "" } func Query(question, title string, headings []string) string { if q := strings.TrimSpace(question); q != "" { return q } var b strings.Builder b.WriteString(title) n := 0 for _, h := range headings { if n >= 3 { break } if strings.TrimSpace(h) == "" { continue } b.WriteByte(' ') b.WriteString(h) n++ } return strings.TrimSpace(b.String()) } func (c *Client) String() string { return fmt.Sprintf("fleetctx base=%s", c.Base) } // CallerWS is a keys.toml name+workspace pair for startup/reload checks. type CallerWS struct { Name, Workspace string } // CheckMappings logs ERROR for every permitted caller whose workspace is not // Restricted, and WARN for every permit_restricted name with no key at all. func (c *Client) CheckMappings(callers []CallerWS) { if c == nil { return } have := make(map[string]struct{}, len(callers)) for _, a := range callers { have[a.Name] = struct{}{} if a.Name == auth.NameMetrics { continue } if permitted(a.Name, c.Permit) && !restricted(a.Workspace) { if c.Log != nil { c.Log.Error("restricted caller mapped off guardian-star", "caller", a.Name, "workspace", a.Workspace) } } } for _, name := range c.Permit { if _, ok := have[name]; !ok { if c.Log != nil { c.Log.Warn("permit_restricted caller has no key", "caller", name) } } } }