Canonicalize export redaction labels

This commit is contained in:
rcourtman
2026-03-19 04:07:47 +00:00
parent b8a77cf726
commit f6a06866cc
7 changed files with 44 additions and 28 deletions
@@ -145,6 +145,10 @@ That export decision must come from the shared unified-resource privacy
helpers, so sensitivity floors and redaction-triggered routing stay aligned
with the canonical policy contract instead of being recomputed in AI-local
code.
The export audit should also record canonical human-readable redaction labels
from the shared policy presentation helper, so the audit trail and the
resource-context surfaces speak the same governed redaction language instead
of reformatting hint names locally.
The same AI runtime boundary now also consumes the canonical unified-resource
timeline when it assembles rich resource or incident context. Recent-change
context should come from the shared resource store first so AI prompts reflect
@@ -448,6 +448,10 @@ Shared privacy helpers also own the export sensitivity floor and route
decision derived from those canonical policy counts, so AI export audits and
route decisions reuse the same governed boundary logic instead of rebuilding
it in consumer packages.
That same export path now records canonical human-readable redaction labels
through the shared policy presentation helper, so audit records and prompt
context stay aligned on the same redaction vocabulary instead of duplicating
hint-to-label conversion in AI-local code.
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
+1 -27
View File
@@ -5,7 +5,6 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
@@ -42,16 +41,7 @@ func (s *Service) recordUnifiedResourceExport(destinationModel, summary string,
return
}
redactions := make([]string, 0, len(redactionHints))
for _, hint := range redactionHints {
redaction := strings.TrimSpace(string(hint))
if redaction == "" {
continue
}
redactions = append(redactions, redaction)
}
sort.Strings(redactions)
redactions = uniqueStrings(redactions)
redactions := unifiedresources.ResourceRedactionLabelsFromHints(redactionHints)
sensitivityFloor := unifiedresources.ExportSensitivityFloor(sensitivityCounts)
decision, reason := unifiedresources.ExportDecisionForContext(sensitivityFloor, localOnlyCount, len(redactions))
@@ -122,19 +112,3 @@ func (s *Service) recordUnifiedResourceExport(destinationModel, summary string,
Msg("failed to persist unified resource export audit")
}
}
func uniqueStrings(values []string) []string {
if len(values) <= 1 {
return values
}
out := make([]string, 0, len(values))
last := ""
for _, value := range values {
if value == "" || value == last {
continue
}
out = append(out, value)
last = value
}
return out
}
+1 -1
View File
@@ -42,7 +42,7 @@ func TestRecordUnifiedResourceExport_UsesCanonicalPrivacyHelpers(t *testing.T) {
},
wantDecision: unifiedresources.ExportRedacted,
wantReason: "governed unified resource context exported in redacted form",
wantRedactions: []string{"hostname", "path"},
wantRedactions: []string{"Hostname", "Path"},
},
}
@@ -346,6 +346,9 @@ func TestResourcePolicyLabelHelpersUsedByAIConsumers(t *testing.T) {
"unifiedresources.ResourcePolicyLabel(",
"unifiedresources.ResourcePolicyRedactedValue(",
},
filepath.Join("..", "ai", "resource_export.go"): {
"unifiedresources.ResourceRedactionLabelsFromHints(redactionHints)",
},
filepath.Join("..", "ai", "resource_context.go"): {
"unifiedresources.ResourcePolicyLabel(",
"unifiedresources.ResourcePolicyRedactedValue(",
@@ -359,6 +362,7 @@ func TestResourcePolicyLabelHelpersUsedByAIConsumers(t *testing.T) {
"func ResourcePolicyLabel(name, aiSafeSummary string, policy *ResourcePolicy) string",
"func ResourcePolicyRedactedValue(value string, policy *ResourcePolicy, hints ...ResourceRedactionHint) string",
"const ResourcePolicyRedactedLabel = \"redacted by policy\"",
"func ResourceRedactionLabelsFromHints(hints []ResourceRedactionHint) []string",
"func ResourceDisplayName(resource Resource) string",
},
}
@@ -236,6 +236,24 @@ func TestResourcePolicyLabelHelpers(t *testing.T) {
}
}
func TestResourceRedactionLabelsFromHints(t *testing.T) {
got := ResourceRedactionLabelsFromHints([]ResourceRedactionHint{
ResourceRedactionPath,
ResourceRedactionHostname,
ResourceRedactionHostname,
ResourceRedactionAlias,
})
want := []string{"Hostname", "Alias", "Path"}
if len(got) != len(want) {
t.Fatalf("labels = %#v, want %#v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("labels[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestResourceDisplayName(t *testing.T) {
if got := ResourceDisplayName(Resource{
Name: " node-a ",
@@ -97,6 +97,18 @@ func ResourcePolicyRedactionLabels(policy *ResourcePolicy) []string {
return ResourcePolicyRedactionLabelsFromCounts(counts)
}
// ResourceRedactionLabelsFromHints returns canonical human-readable labels for a hint slice.
func ResourceRedactionLabelsFromHints(hints []ResourceRedactionHint) []string {
if len(hints) == 0 {
return nil
}
counts := make(map[ResourceRedactionHint]int, len(hints))
for _, hint := range hints {
counts[hint]++
}
return ResourcePolicyRedactionLabelsFromCounts(counts)
}
// ResourcePolicyRedactionLabelsFromCounts returns the canonical labels for the
// redaction hints present in the provided count map.
func ResourcePolicyRedactionLabelsFromCounts(counts map[ResourceRedactionHint]int) []string {