Files
sift/internal/fleetctx/fleetctx.go
T
kyle e36da19f57 p2.4: restricted workspace for permitted callers
guardian-star context is allowed only when the authenticated caller is in
permit_restricted and mapped to that workspace. Fleet callers remapped to it
still refuse. CLI client.toml: SIFT_CLIENT, then $HERMES_HOME/sift, then ~/.config.
2026-09-23 08:33:58 -07:00

181 lines
4.2 KiB
Go

// 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"
)
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.
func (c *Client) Search(ctx context.Context, workspace, caller, query string) (items []Item, warn string) {
if c == nil || query == "" {
return nil, ""
}
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) }