Surface canonical policy governance posture

This commit is contained in:
rcourtman
2026-03-19 00:53:18 +00:00
parent a4a4f1752f
commit 4ea83e39c1
8 changed files with 150 additions and 2 deletions
@@ -105,6 +105,10 @@ policy-aware metadata contract. The AI runtime may summarize governed resource
policy counts for context, and it must switch to `aiSafeSummary` when a
resource is marked `local-only` instead of leaking raw resource names or local
identifiers for restricted resources through ad hoc context formatting.
That governed context should also surface the canonical routing posture and
redaction hints that were derived from the shared policy model, so prompts
reflect the same sensitivity, routing, and scrub decisions that the runtime
uses for export boundaries instead of rebuilding privacy posture locally.
That same policy boundary now applies to chat structured-mention prefetch and
resource-summary formatting: mention resolution must consume canonical
unified-resource policy metadata, skip discovery fan-out when governed
@@ -100,3 +100,6 @@ The backend Patrol and AI runtime summaries now also share
`internal/unifiedresources/change_presentation.go` for the canonical
change-kind and provenance mapping, so the same resource-model semantics
drive both the backend summaries and the frontend presentation helpers.
The canonical shared AI resource context now also surfaces policy routing and
redaction hints from unified resources, so the Patrol page and resource drawer
see the same governance posture that the runtime uses for export boundaries.
@@ -373,6 +373,10 @@ redaction hints for hostname, IP, platform-identity, alias, and path-bearing
surfaces. Downstream API, AI, and frontend consumers may read those fields,
but they must not replace them with local sensitivity inference or ad hoc
privacy heuristics.
The AI runtime now also uses the canonical policy presentation helpers to
surface those routing and redaction labels in shared context output, so the
same policy model is reflected in prompt summaries instead of being
re-described independently per surface.
Canonical resources now carry first-class graph-expansion fields: `Capabilities`
(bounded action definitions with approval levels), `Relationships` (typed
inter-resource links with direction and confidence), and `RecentChanges` (typed
+22 -1
View File
@@ -106,6 +106,7 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s
byResourceID[resource.ID] = resource
}
sensitivityCounts := make(map[unifiedresources.ResourceSensitivity]int)
routingCounts := make(map[unifiedresources.ResourceRoutingScope]int)
localOnlyCount := 0
var redactionHints []unifiedresources.ResourceRedactionHint
redactionHintSet := make(map[unifiedresources.ResourceRedactionHint]struct{})
@@ -115,6 +116,7 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s
continue
}
sensitivityCounts[resource.Policy.Sensitivity]++
routingCounts[resource.Policy.Routing.Scope]++
if resource.Policy.Routing.Scope == unifiedresources.ResourceRoutingScopeLocalOnly {
localOnlyCount++
}
@@ -133,11 +135,17 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s
}
if len(sensitivityCounts) > 0 {
sections = append(sections, "\n### Data Governance")
sections = append(sections, fmt.Sprintf("- Sensitivity: %d internal, %d sensitive, %d restricted",
sections = append(sections, fmt.Sprintf("- Sensitivity: %d public, %d internal, %d sensitive, %d restricted",
sensitivityCounts[unifiedresources.ResourceSensitivityPublic],
sensitivityCounts[unifiedresources.ResourceSensitivityInternal],
sensitivityCounts[unifiedresources.ResourceSensitivitySensitive],
sensitivityCounts[unifiedresources.ResourceSensitivityRestricted],
))
sections = append(sections, fmt.Sprintf("- Routing: %d cloud-summary, %d local-first, %d local-only",
routingCounts[unifiedresources.ResourceRoutingScopeCloudSummary],
routingCounts[unifiedresources.ResourceRoutingScopeLocalFirst],
routingCounts[unifiedresources.ResourceRoutingScopeLocalOnly],
))
sections = append(sections, fmt.Sprintf("- Local-only resources: %d", localOnlyCount))
}
}
@@ -518,6 +526,19 @@ func (s *Service) buildUnifiedResourceContextForModel(destinationModel string) s
}
}
if len(redactionHints) > 0 {
sections = append(sections, "\n### Policy Redaction Hints")
labels := make([]string, 0, len(redactionHints))
for _, hint := range redactionHints {
if label := unifiedresources.ResourceRedactionHintLabel(hint); label != "" {
labels = append(labels, label)
}
}
if len(labels) > 0 {
sections = append(sections, fmt.Sprintf("- Redactions in use: %s", strings.Join(labels, ", ")))
}
}
result := "\n\n" + strings.Join(sections, "\n")
const maxContextSize = 50000
+4 -1
View File
@@ -222,7 +222,10 @@ func TestBuildUnifiedResourceContext_FullContext(t *testing.T) {
assertContains("## Unified Infrastructure View")
assertContains("Total resources: 11 (Infrastructure: 5, Workloads: 6)")
assertContains("Data Governance")
assertContains("Sensitivity: 5 internal, 6 sensitive, 0 restricted")
assertContains("Sensitivity: 0 public, 5 internal, 6 sensitive, 0 restricted")
assertContains("Routing: 5 cloud-summary, 6 local-first, 0 local-only")
assertContains("Policy Redaction Hints")
assertContains("Redactions in use: Alias, Hostname, IP Address, Platform ID")
assertContains("Proxmox VE Nodes")
assertContains("HAS AGENT")
assertContains("NO AGENT")
@@ -264,6 +264,26 @@ func TestResourceAPIExposesDedicatedFacetReads(t *testing.T) {
}
}
func TestResourcePolicyPresentationUsesCanonicalLabels(t *testing.T) {
data, err := os.ReadFile(filepath.Join(".", "policy_presentation.go"))
if err != nil {
t.Fatalf("failed to read policy_presentation.go: %v", err)
}
source := string(data)
requiredSnippets := []string{
"ResourceSensitivityLabel(",
"ResourceRoutingScopeLabel(",
"ResourceRedactionHintLabel(",
"ResourcePolicyRedactionLabels(",
}
for _, snippet := range requiredSnippets {
if !strings.Contains(source, snippet) {
t.Fatalf("policy_presentation.go must contain %q", snippet)
}
}
}
func TestResourceTimelineStoreIndexesSupportFilteredReads(t *testing.T) {
data, err := os.ReadFile(filepath.Join("store.go"))
if err != nil {
@@ -79,6 +79,31 @@ func TestRefreshPolicyMetadata_ClassifiesInfrastructureAsInternal(t *testing.T)
}
}
func TestResourcePolicyPresentationLabels(t *testing.T) {
if got := ResourceSensitivityLabel(ResourceSensitivityPublic); got != "Public" {
t.Fatalf("sensitivity label = %q, want %q", got, "Public")
}
if got := ResourceRoutingScopeLabel(ResourceRoutingScopeLocalOnly); got != "Local Only" {
t.Fatalf("routing label = %q, want %q", got, "Local Only")
}
if got := ResourceRedactionHintLabel(ResourceRedactionIPAddress); got != "IP Address" {
t.Fatalf("redaction label = %q, want %q", got, "IP Address")
}
policy := &ResourcePolicy{
Routing: ResourceRoutingPolicy{
Redact: []ResourceRedactionHint{
ResourceRedactionPath,
ResourceRedactionHostname,
},
},
}
got := ResourcePolicyRedactionLabels(policy)
if len(got) != 2 || got[0] != "Hostname" || got[1] != "Path" {
t.Fatalf("redaction labels = %#v, want [Hostname Path]", got)
}
}
func containsRedactionHint(hints []ResourceRedactionHint, want ResourceRedactionHint) bool {
for _, hint := range hints {
if hint == want {
@@ -0,0 +1,68 @@
package unifiedresources
import "sort"
// ResourceSensitivityLabel returns the canonical human-readable sensitivity label.
func ResourceSensitivityLabel(sensitivity ResourceSensitivity) string {
switch sensitivity {
case ResourceSensitivityPublic:
return "Public"
case ResourceSensitivityInternal:
return "Internal"
case ResourceSensitivitySensitive:
return "Sensitive"
case ResourceSensitivityRestricted:
return "Restricted"
default:
return "Unclassified"
}
}
// ResourceRoutingScopeLabel returns the canonical human-readable routing scope label.
func ResourceRoutingScopeLabel(scope ResourceRoutingScope) string {
switch scope {
case ResourceRoutingScopeCloudSummary:
return "Cloud Summary"
case ResourceRoutingScopeLocalFirst:
return "Local First"
case ResourceRoutingScopeLocalOnly:
return "Local Only"
default:
return "Unrouted"
}
}
// ResourceRedactionHintLabel returns the canonical human-readable redaction label.
func ResourceRedactionHintLabel(hint ResourceRedactionHint) string {
switch hint {
case ResourceRedactionHostname:
return "Hostname"
case ResourceRedactionIPAddress:
return "IP Address"
case ResourceRedactionPlatformID:
return "Platform ID"
case ResourceRedactionAlias:
return "Alias"
case ResourceRedactionPath:
return "Path"
default:
return string(hint)
}
}
// ResourcePolicyRedactionLabels returns the canonical human-readable labels for a policy's redaction hints.
func ResourcePolicyRedactionLabels(policy *ResourcePolicy) []string {
if policy == nil || len(policy.Routing.Redact) == 0 {
return nil
}
labels := make([]string, 0, len(policy.Routing.Redact))
for _, hint := range policy.Routing.Redact {
label := ResourceRedactionHintLabel(hint)
if label == "" {
continue
}
labels = append(labels, label)
}
sort.Strings(labels)
return labels
}