Files
Agent-Intent/intent/slots.go
T
weichen 765a7e7177 Isolate snapshots and validate generated commands
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
2026-10-21 15:41:31 +00:00

261 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intent
import (
"regexp"
"strings"
"sync"
)
// SlotValidator 宿主注册的槽位值校验器。必须纯函数、确定性、无 I/O。
type SlotValidator func(value string) error
var slotValidators sync.Map // name → SlotValidator
// RegisterSlotValidator 注册进程级具名校验器;spec 通过 SlotSpec.Validator 引用。
// 必须在快照构建前注册(构建期会校验名字存在)。
//
// Deprecated: 校验器应随 Registry 隔离。请改用 Options.SlotValidators。
// 全局表只作回落:构建与校验先查作用域表,查不到再查这里。
func RegisterSlotValidator(name string, fn SlotValidator) {
slotValidators.Store(name, fn)
}
// LookupSlotValidator 按名查找已注册的全局校验器。
//
// Deprecated: 请改用 Options.SlotValidators。本函数只服务全局回落。
func LookupSlotValidator(name string) (SlotValidator, bool) {
fn, ok := slotValidators.Load(name)
if !ok {
return nil, false
}
v, ok := fn.(SlotValidator)
return v, ok
}
// resolveSlotValidator 先查作用域表,查不到再回落已弃用的全局表。
func resolveSlotValidator(scoped map[string]SlotValidator, name string) (SlotValidator, bool) {
if fn, ok := scoped[name]; ok && fn != nil {
return fn, true
}
return LookupSlotValidator(name)
}
// 内置归一化步骤:trim / lower / upper。
var builtinNormalizers = map[string]func(string) string{
"trim": strings.TrimSpace,
"lower": strings.ToLower,
"upper": strings.ToUpper,
}
func normalizeSlotValue(v string, ops []string) string {
for _, op := range ops {
if fn, ok := builtinNormalizers[op]; ok {
v = fn(v)
}
}
return v
}
// compiledSlot 编译后的槽位抽取与校验产物。
type compiledSlot struct {
key string
required bool
label string
question string
re *regexp.Regexp
group int
enumSet map[string]bool
enum []string
formatRe *regexp.Regexp
normalize []string
validator SlotValidator
}
// validate 对归一化后的值做确定性校验;返回(原因, 是否合法)。
func (s *compiledSlot) validate(v string) (string, bool) {
if s.formatRe != nil && !s.formatRe.MatchString(v) {
return "format mismatch: " + s.formatRe.String(), false
}
if s.enumSet != nil && !s.enumSet[v] {
return "value not in enum", false
}
if s.validator != nil {
if err := s.validator(v); err != nil {
return "validator: " + err.Error(), false
}
}
return "", true
}
// slotCapture 一次槽位抽取的捕获区间。多槽位各自第一次匹配;区间重叠时
// 两边都标 span overlap,不写入 slots。库内不做重叠消解。
type slotCapture struct {
key string
start int
end int
value string
ok bool
reason string
required bool
matched bool
}
// extractSlots 抽取并归一化槽位。
// missing:必填槽位未捕获(模式没匹配上);invalid:捕获到但校验不合法,或与另一槽位区间重叠(key→原因)。
// 必填且非法只进 invalid(宿主可区分"没给"与"给错了"来决定澄清话术)。
func (cs *compiledSpec) extractSlots(input string) (slots map[string]string, missing []string, invalid map[string]string) {
if len(cs.slots) == 0 {
return nil, nil, nil
}
caps := make([]slotCapture, 0, len(cs.slots))
for _, s := range cs.slots {
loc := s.re.FindStringSubmatchIndex(input)
g := s.group
if loc == nil || 2*g+1 >= len(loc) || loc[2*g] < 0 || loc[2*g] == loc[2*g+1] {
caps = append(caps, slotCapture{key: s.key, required: s.required})
continue
}
raw := input[loc[2*g]:loc[2*g+1]]
v := normalizeSlotValue(raw, s.normalize)
reason, ok := s.validate(v)
caps = append(caps, slotCapture{
key: s.key, start: loc[2*g], end: loc[2*g+1], value: v,
ok: ok, reason: reason, required: s.required, matched: true,
})
}
overlap := map[int]bool{}
for i := range caps {
if !caps[i].matched {
continue
}
for j := i + 1; j < len(caps); j++ {
if !caps[j].matched {
continue
}
if caps[i].start < caps[j].end && caps[j].start < caps[i].end {
overlap[i] = true
overlap[j] = true
}
}
}
for i, c := range caps {
if !c.matched {
if c.required {
missing = append(missing, c.key)
}
continue
}
if overlap[i] {
if invalid == nil {
invalid = map[string]string{}
}
invalid[c.key] = "span overlap"
continue
}
if !c.ok {
if invalid == nil {
invalid = map[string]string{}
}
invalid[c.key] = c.reason
continue
}
if slots == nil {
slots = map[string]string{}
}
slots[c.key] = c.value
}
return slots, missing, invalid
}
// validateProvidedSlots 校验调用方直接给出的槽位表(生成器路径)。
// 与 extractSlots 同款结果形态:归一化 → format/enum/validator;必填缺失进 missing;
// 捕获到但不合法进 invalid;未知键进 invalid(reason=unknown slot key)。
// 非法或未知键的值不写入 slots。
func (cs *compiledSpec) validateProvidedSlots(provided map[string]string) (slots map[string]string, missing []string, invalid map[string]string) {
byKey := make(map[string]*compiledSlot, len(cs.slots))
for i := range cs.slots {
byKey[cs.slots[i].key] = &cs.slots[i]
}
for k, raw := range provided {
s, ok := byKey[k]
if !ok {
if invalid == nil {
invalid = map[string]string{}
}
invalid[k] = "unknown slot key"
continue
}
v := normalizeSlotValue(raw, s.normalize)
reason, valid := s.validate(v)
if !valid {
if invalid == nil {
invalid = map[string]string{}
}
invalid[k] = reason
continue
}
if slots == nil {
slots = map[string]string{}
}
slots[k] = v
}
for _, s := range cs.slots {
if !s.required {
continue
}
if _, ok := slots[s.key]; ok {
continue
}
// 必填且已标 invalid 的不重复进 missing(与 extractSlots 同款)。
if invalid != nil {
if _, bad := invalid[s.key]; bad {
continue
}
}
missing = append(missing, s.key)
}
return slots, missing, invalid
}
func compileSlot(spec SlotSpec, scoped map[string]SlotValidator) (compiledSlot, error) {
re, err := regexp.Compile(spec.Pattern)
if err != nil {
return compiledSlot{}, newError(InvalidSpec, "slot pattern 编译失败",
"field", "slots", "key", spec.Key, "error", err.Error())
}
cs := compiledSlot{
key: spec.Key,
required: spec.Required,
label: spec.Label,
question: spec.Question,
re: re,
group: effectiveGroup(spec.Group),
normalize: append([]string(nil), spec.Normalize...),
}
if len(spec.Enum) > 0 {
cs.enum = append([]string(nil), spec.Enum...)
cs.enumSet = make(map[string]bool, len(spec.Enum))
for _, e := range spec.Enum {
cs.enumSet[e] = true
}
}
if spec.Format != "" {
fre, err := regexp.Compile(spec.Format)
if err != nil {
return compiledSlot{}, newError(InvalidSpec, "slot format 编译失败",
"field", "slots", "key", spec.Key, "format", spec.Format, "error", err.Error())
}
cs.formatRe = fre
}
if spec.Validator != "" {
fn, ok := resolveSlotValidator(scoped, spec.Validator)
if !ok {
return compiledSlot{}, newError(InvalidSpec, "slot validator 未注册",
"field", "slots", "key", spec.Key, "validator", spec.Validator,
"hint", "在 Options.SlotValidators 登记,或调用已弃用的 intent.RegisterSlotValidator")
}
cs.validator = fn
}
return cs, nil
}