mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Surface canonical policy governance posture
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user