package skills import ( "fmt" "path" "strings" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // Validate 校验技能定义的结构约束(design/09 §6.2 的「校验(必做,错误可分类)」): // // - steps[].name 非空、不重复(重复 ⇒ ErrStepDuplicate); // - steps[].depends 指向存在的步骤(否则 ErrStepDependsMissing); // - 步骤依赖无环(有环 ⇒ ErrStepCycle,**加载期就拒,运行期绝不挂死**); // - allowed-tools 与 disallowed-tools 无同名(否则 ErrToolConflict); // - guardrails[].id 不重复(否则 ErrGuardrailDuplicate); // - invocation / context 取值合法(否则 sdk.ErrInvalidSkillField); // - paths 的 glob 语法合法(否则 sdk.ErrInvalidSkillField)。 // // 加载器在解析后调用它(Load/LoadFile/LoadDir 都过这一关),Runner 在开工前 // 再调用一次 —— 校验器对外部直接构造的 sdk.Skill 同样有效。 func Validate(s sdk.Skill) error { if s == nil || strings.TrimSpace(s.Name()) == "" { return fmt.Errorf("skills: validate: %w", ErrInvalidSkill) } name := s.Name() spec := sdk.SkillView(s) if !spec.Invocation().Valid() { return fmt.Errorf("skills: validate %q: invocation %q: %w", name, spec.Invocation(), sdk.ErrInvalidSkillField) } if !spec.ContextMode().Valid() { return fmt.Errorf("skills: validate %q: context %q: %w", name, spec.ContextMode(), sdk.ErrInvalidSkillField) } if err := validatePaths(name, spec.Paths()); err != nil { return err } if err := validateToolDecls(name, spec.AllowedTools(), spec.DisallowedTools()); err != nil { return err } if err := validateGuardrails(name, s.Guardrails()); err != nil { return err } _, err := TopoOrder(s.Steps()) if err != nil { return fmt.Errorf("skills: validate %q: %w", name, err) } return nil } // validatePaths 检查每个 glob 的语法(用 OS 无关的 path.Match 做语法体检:它只 // 报语法错,不做真匹配)。语法错会让「按工作文件自动激活」永远不命中,属于静默 // 失效,因此在加载期就拒。 func validatePaths(name string, patterns []string) error { for _, p := range patterns { if _, err := path.Match(p, ""); err != nil { return fmt.Errorf("skills: validate %q: paths %q: %w", name, p, sdk.ErrInvalidSkillField) } } return nil } // validateToolDecls 拒绝「同一工具同时被允许与被禁止」的自相矛盾声明。 func validateToolDecls(name string, allowed, disallowed []string) error { if len(allowed) == 0 || len(disallowed) == 0 { return nil } allowedSet := make(map[string]struct{}, len(allowed)) for _, tool := range allowed { allowedSet[strings.TrimSpace(tool)] = struct{}{} } for _, tool := range disallowed { if _, hit := allowedSet[strings.TrimSpace(tool)]; hit { return fmt.Errorf("skills: validate %q: tool %q: %w", name, tool, ErrToolConflict) } } return nil } // validateGuardrails 拒绝重复的 guardrail id(重复让「哪条 guardrail」说不清)。 func validateGuardrails(name string, guardrails []sdk.Guardrail) error { seen := make(map[string]struct{}, len(guardrails)) for _, g := range guardrails { id := strings.TrimSpace(g.ID) if _, dup := seen[id]; dup { return fmt.Errorf("skills: validate %q: guardrail %q: %w", name, g.ID, ErrGuardrailDuplicate) } seen[id] = struct{}{} } return nil } // TopoOrder 返回按依赖拓扑排序后的步骤名。就绪步骤按**声明顺序**出队,因此输出 // 是确定的(同样的定义永远得到同样的顺序)。有任何一步还没排进去就说明依赖成环, // 此时返回 ErrStepCycle 并如实列出未排入的步骤名。 // // 步骤名重复、为空、或 depends 指向不存在的步骤,都会先在这里被拒。 func TopoOrder(steps []sdk.Step) ([]string, error) { if len(steps) == 0 { return nil, nil } index := make(map[string]int, len(steps)) remaining := make([]int, len(steps)) dependents := make([][]int, len(steps)) for i, st := range steps { name := strings.TrimSpace(st.Name) if name == "" { return nil, fmt.Errorf("skills: topo order: step #%d has a blank name: %w", i, ErrInvalidSkill) } if _, dup := index[name]; dup { return nil, fmt.Errorf("skills: topo order: step %q: %w", name, ErrStepDuplicate) } index[name] = i } for i, st := range steps { for _, dep := range st.Depends { depIdx, ok := index[strings.TrimSpace(dep)] if !ok { return nil, fmt.Errorf("skills: topo order: step %q depends on unknown step %q: %w", st.Name, dep, ErrStepDependsMissing) } remaining[i]++ dependents[depIdx] = append(dependents[depIdx], i) } } queue := make([]int, 0, len(steps)) for i := range steps { if remaining[i] == 0 { queue = append(queue, i) } } order := make([]string, 0, len(steps)) for len(queue) > 0 { idx := queue[0] queue = queue[1:] order = append(order, steps[idx].Name) for _, dep := range dependents[idx] { remaining[dep]-- if remaining[dep] == 0 { queue = append(queue, dep) } } } if len(order) != len(steps) { return nil, fmt.Errorf("skills: topo order: steps %v: %w", unsortedStepNames(steps, remaining), ErrStepCycle) } return order, nil } // unsortedStepNames 收集没排进拓扑序的步骤名(按声明序,错误信息因此确定)。 func unsortedStepNames(steps []sdk.Step, remaining []int) []string { out := make([]string, 0, len(steps)) for i, st := range steps { if remaining[i] > 0 { out = append(out, st.Name) } } return out }