// Package fleetctx is Honcho retrieval for sift. Read-only. Never guardian-star. package fleetctx import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "net/http" "net/url" "regexp" "strings" "sync/atomic" "time" "unicode/utf8" ) const ( Forbidden = "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 HTTP *http.Client Log *slog.Logger Calls atomic.Uint64 Hits atomic.Uint64 Failures atomic.Uint64 } func refused(ws string) bool { return strings.EqualFold(strings.TrimSpace(ws), Forbidden) } var workspaceName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) func workspaceOK(ws string) bool { return workspaceName.MatchString(ws) } func allowed(ws string, allow []string) bool { if refused(ws) { return false } for _, a := range allow { if a == ws { return true } } return false } // Search returns prior fleet knowledge. Failure → (nil, warning). Never an error // that fails the fetch. guardian-star is refused in code, not only config. func (c *Client) Search(ctx context.Context, workspace, query string) (items []Item, warn string) { if c == nil || query == "" { return nil, "" } if refused(workspace) { if c.Log != nil { c.Log.Warn("context refused", "workspace", workspace) } 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) }