diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 74b2ac9eb..0f580fed1 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 83ce98d43..e1871dfbd 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -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 diff --git a/internal/ai/resource_export.go b/internal/ai/resource_export.go index 03985080c..3a5815bb3 100644 --- a/internal/ai/resource_export.go +++ b/internal/ai/resource_export.go @@ -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 -} diff --git a/internal/ai/resource_export_test.go b/internal/ai/resource_export_test.go index fa40b28a5..18940f4e2 100644 --- a/internal/ai/resource_export_test.go +++ b/internal/ai/resource_export_test.go @@ -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"}, }, } diff --git a/internal/unifiedresources/code_standards_test.go b/internal/unifiedresources/code_standards_test.go index 52b15b653..1f51a1a69 100644 --- a/internal/unifiedresources/code_standards_test.go +++ b/internal/unifiedresources/code_standards_test.go @@ -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", }, } diff --git a/internal/unifiedresources/policy_metadata_test.go b/internal/unifiedresources/policy_metadata_test.go index b0c76b09c..b0e5dd123 100644 --- a/internal/unifiedresources/policy_metadata_test.go +++ b/internal/unifiedresources/policy_metadata_test.go @@ -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 ", diff --git a/internal/unifiedresources/policy_presentation.go b/internal/unifiedresources/policy_presentation.go index 62d86a297..5992da997 100644 --- a/internal/unifiedresources/policy_presentation.go +++ b/internal/unifiedresources/policy_presentation.go @@ -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 {