Why: F07/F08 - registry and catalog accessors shared caller-mutable nested state, and the generation fallback skipped full slot validation, risk metadata and mis-keyed handler slot tables. Evidence: intent package tests pass incl. new isolation/generated cases (verify-u5.json). Impact: no exported signature change; rule and generated paths now share authoritative validation. Refs: feature/agent-stack-audit-remediation-b1 U5
162 lines
3.8 KiB
Go
162 lines
3.8 KiB
Go
package intent
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// foldMatchText 做识别用折叠。nfkc 只做兼容等价(全角 ASCII、组合字符),
|
|
// 不做繁简转换。caseSensitive 为真时保留大小写。
|
|
func foldMatchText(s string, nfkc, caseSensitive bool) string {
|
|
if nfkc {
|
|
s = norm.NFKC.String(s)
|
|
}
|
|
if !caseSensitive {
|
|
s = strings.ToLower(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// foldInput 返回用于匹配的文本:第一段保留大小写(可选 NFKC),第二段是其小写。
|
|
func (s *Snapshot) foldInput(input string) (matchInput, folded string) {
|
|
if s == nil {
|
|
return input, strings.ToLower(input)
|
|
}
|
|
matchInput = foldMatchText(input, s.nfkc, true)
|
|
return matchInput, strings.ToLower(matchInput)
|
|
}
|
|
|
|
func cloneStrings(in []string) []string {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
return append([]string(nil), in...)
|
|
}
|
|
|
|
func cloneStringMap(in map[string]string) map[string]string {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneActionRef(in *ActionRef) *ActionRef {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
cp := *in
|
|
return &cp
|
|
}
|
|
|
|
// cloneIntentSpec 深拷贝 IntentSpec,切断切片/map/指针与调用方或快照内部的共享。
|
|
func cloneIntentSpec(in IntentSpec) IntentSpec {
|
|
out := in
|
|
out.Matcher.IncludesAny = cloneStrings(in.Matcher.IncludesAny)
|
|
out.Matcher.IncludesAll = cloneStrings(in.Matcher.IncludesAll)
|
|
out.Matcher.ExcludesAny = cloneStrings(in.Matcher.ExcludesAny)
|
|
out.Matcher.Patterns = cloneStrings(in.Matcher.Patterns)
|
|
if in.Slots != nil {
|
|
out.Slots = append([]SlotSpec(nil), in.Slots...)
|
|
for i := range out.Slots {
|
|
out.Slots[i].Enum = cloneStrings(in.Slots[i].Enum)
|
|
out.Slots[i].Normalize = cloneStrings(in.Slots[i].Normalize)
|
|
}
|
|
}
|
|
out.Metadata = cloneStringMap(in.Metadata)
|
|
out.Action = cloneActionRef(in.Action)
|
|
out.Examples = cloneStrings(in.Examples)
|
|
return out
|
|
}
|
|
|
|
func cloneCatalogSlot(in CatalogSlot) CatalogSlot {
|
|
out := in
|
|
out.Enum = cloneStrings(in.Enum)
|
|
return out
|
|
}
|
|
|
|
func cloneCatalogSpec(in CatalogSpec) CatalogSpec {
|
|
out := in
|
|
out.Metadata = cloneStringMap(in.Metadata)
|
|
if in.Slots != nil {
|
|
out.Slots = make([]CatalogSlot, len(in.Slots))
|
|
for i := range in.Slots {
|
|
out.Slots[i] = cloneCatalogSlot(in.Slots[i])
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneCatalogSpecs(in []CatalogSpec) []CatalogSpec {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]CatalogSpec, len(in))
|
|
for i := range in {
|
|
out[i] = cloneCatalogSpec(in[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneOptions(in Options) Options {
|
|
out := in
|
|
out.ProtectedDomains = cloneStrings(in.ProtectedDomains)
|
|
out.GuardDomains = cloneStrings(in.GuardDomains)
|
|
if in.SlotValidators != nil {
|
|
cp := make(map[string]SlotValidator, len(in.SlotValidators))
|
|
for k, v := range in.SlotValidators {
|
|
cp[k] = v
|
|
}
|
|
out.SlotValidators = cp
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneCandidate(in Candidate) Candidate {
|
|
out := in
|
|
out.Metadata = cloneStringMap(in.Metadata)
|
|
out.Slots = cloneStringMap(in.Slots)
|
|
out.MissingRequiredSlots = cloneStrings(in.MissingRequiredSlots)
|
|
out.InvalidSlots = cloneStringMap(in.InvalidSlots)
|
|
out.Action = cloneActionRef(in.Action)
|
|
return out
|
|
}
|
|
|
|
func cloneCandidates(in []Candidate) []Candidate {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]Candidate, len(in))
|
|
for i := range in {
|
|
out[i] = cloneCandidate(in[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// catalogMetadataAllow 是目录与 Markdown 允许带出的 metadata 键。
|
|
// handler 仍留在 CatalogSpec 供执行器按名分派,但不写入 Markdown。
|
|
var catalogMetadataAllow = map[string]bool{
|
|
"blueprint": true,
|
|
"guidance": true,
|
|
}
|
|
|
|
func catalogMetadata(in map[string]string) map[string]string {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := map[string]string{}
|
|
for k, v := range in {
|
|
if catalogMetadataAllow[k] {
|
|
out[k] = v
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|