p2.6: partition dcache by caller class; pin restricted workspace

Digest, chunk-embed, and question-embed keys include fleet|restricted so
a fleet caller cannot observe a restricted cache hit. Digest.Run takes
the class from permit_restricted.

A permit_restricted caller whose keys.toml workspace is not
guardian-star is refused at Search (no Honcho request) and flagged at
serve start and keys reload.

docs: P3 restricted callers search like the fleet; cache/logs isolate.
This commit is contained in:
2026-09-23 11:51:39 -07:00
parent 7be4ed1d43
commit 3687636ed7
12 changed files with 202 additions and 67 deletions
+29 -1
View File
@@ -76,11 +76,20 @@ func allowed(ws string, allow []string) bool {
}
// Search returns prior knowledge for workspace. Failure → (nil, warning). Never
// an error that fails the fetch. Restricted workspace requires caller ∈ Permit.
// 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)
@@ -178,3 +187,22 @@ func Query(question, title string, headings []string) 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.
func (c *Client) CheckMappings(callers []CallerWS) {
if c == nil {
return
}
for _, a := range callers {
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)
}
}
}
}
+24
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
)
@@ -30,6 +31,29 @@ func TestRestrictedRefusedWhenFleetCallerRemapped(t *testing.T) {
}
}
func TestRestrictedCallerMappedToFleetMakesNoRequest(t *testing.T) {
var hits atomic.Int32
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(200)
_, _ = w.Write([]byte("[]"))
}))
defer ts.Close()
c := &Client{
Base: ts.URL,
Allow: []string{"guardian-star", "hermes-fleet"},
Permit: []string{"straylight-gs"},
HTTP: ts.Client(),
}
items, warn := c.Search(context.Background(), "hermes-fleet", "straylight-gs", "secret query")
if hits.Load() != 0 {
t.Fatalf("honcho hits %d", hits.Load())
}
if len(items) != 0 || warn == "" {
t.Fatalf("items %v warn %q", items, warn)
}
}
func TestRestrictedAllowedForPermittedCaller(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v3/workspaces/guardian-star/search" {