Files
Cyrene_v2/internal/capability/skills/surface.go
T
AskaEth f75e7a9c9b feat(skills): 技能加载/校验/真执行第一片(design/09 §6 定稿落地)
- 加载器/校验:目录式 configs/skills/<id>/skill.yaml(兼容旧单文件 <id>.yaml,先目录后文件);
  新字段 version/when-to-use/invocation(三态)/triggers/paths/allowed-tools/disallowed-tools/
  context(ine|fork)/agent/model/effort/metadata;**依赖环 ⇒ 加载期拒**(运行期绝不挂死);
  depends 指向不存在步骤 ⇒ 拒;同一工具同时在 allowed 与 disallowed ⇒ 拒;错误可分类(哨兵 + %w)
- Runner.Run 真执行(替换 ErrRunPending):步骤拓扑(无依赖可并行、上限 2)+ guardrail 强制点
  (enforced 违反 ⇒ 中止并如实报「哪条、哪一步」;否则 WARN 继续)+ 失败不静默跳过 + ctx 取消传播
- 🔴 能力收缩只收不放:该轮可用工具 = allowed-tools ∩ 安全闸 ∩ 注册表,再减 disallowed-tools
- 另有渐进披露相关的 listing/surface/activation(清单投影与激活判定)

⚠️ 未接线:本片是库与执行器,尚未把技能清单接进模型面(见 report 的分阶段)。
2026-10-06 09:03:35 +08:00

136 lines
4.4 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 skills
import (
"sort"
"strings"
)
// 本文件实现 design/09 §6.4-3 的「工具面 = 求交集(只收不放)」:
//
// 技能轮次可用工具 = allowed-tools(若声明)∩ 当前安全闸 ∩ 全局注册表
// − disallowed-tools
//
// 🔴 铁律:**任何情况下都不得因为技能而放宽**。因此求值函数只从「注册表给出的
// 全集」里**删**,绝不往里**加**;安全闸缺位(nil)时结果是空集(fail-closed)。
//
// 与 Claude Code 的刻意差异(见 design/09 §6.0 对照表):它的 allowed-tools 语义
// 是「免审批」,明说 *It does not restrict which tools are available*;我们的
// allowed-tools 是**可用工具的上限(子集)**,敏感/写操作仍需代审 + 审计。
// Gate 是当前回合的安全闸:actor 元数据 + `ToolPolicy` + 唤醒态允许集。
// 它由运行层(loop)构造后注入 —— 技能包不自己判定身份,也不缓存判定结果。
//
// 实现必须幂等且并发安全(步骤并行时会同时被问)。
type Gate interface {
// Allow 报告名为 name 的工具在当前安全闸下是否可用。
Allow(name string) bool
}
// GateFunc 把普通函数适配成 Gate。
type GateFunc func(name string) bool
// Allow 实现 Gate;nil 函数一律拒(fail-closed)。
func (f GateFunc) Allow(name string) bool { return f != nil && f(name) }
// NameSet 是按名字集合判定的 Gate —— 唤醒态允许集的自然形态。
type NameSet map[string]struct{}
// NewNameSet 把名字列表转成 NameSet(去空白、忽略空串)。
func NewNameSet(names []string) NameSet {
set := make(NameSet, len(names))
for _, name := range names {
if trimmed := strings.TrimSpace(name); trimmed != "" {
set[trimmed] = struct{}{}
}
}
return set
}
// Allow 实现 Gate:只有显式在集合里的名字才可用。
func (s NameSet) Allow(name string) bool {
if s == nil {
return false
}
_, ok := s[name]
return ok
}
// AllOf 把多个闸串成「全允许才允许」,用于把 actor 判定、ToolPolicy 判定与唤醒
// 允许集叠在一个 Gate 上喂给 ResolveToolSurface。
//
// **没有任何一个闸(gates 全 nil/空)时一律全拒**:宁可什么都不给,也不给一个
// 无人看守的工具面。
func AllOf(gates ...Gate) Gate {
effective := make([]Gate, 0, len(gates))
for _, g := range gates {
if g != nil {
effective = append(effective, g)
}
}
if len(effective) == 0 {
return GateFunc(nil)
}
return GateFunc(func(name string) bool {
for _, g := range effective {
if !g.Allow(name) {
return false
}
}
return true
})
}
// ResolveToolSurface 计算技能轮次实际可用的工具名。
//
// 参数:
// - registered:全局注册表给出的工具名全集(本轮 dispatcher 真正持有的工具);
// - allowed:技能的 allowed-tools;**空表示技能不额外收窄**(仍受安全闸约束);
// - disallowed:技能的 disallowed-tools,最后减掉;
// - gate:当前安全闸;nil ⇒ 结果为空集(缺闸即全拒)。
//
// 结果按 registered 的顺序输出(去重、跳过空白名),因此调用方传入确定序就得到
// 确定序 —— 函数本身不排序,避免打乱 dispatcher 的既有工具次序。
func ResolveToolSurface(registered, allowed, disallowed []string, gate Gate) []string {
if gate == nil || len(registered) == 0 {
// 缺闸或没有注册表 ⇒ 没有任何工具可用。绝不「因为技能没声明上限就放行」。
return nil
}
allowedSet := NewNameSet(allowed)
disallowedSet := NewNameSet(disallowed)
hasAllowed := len(allowedSet) > 0
out := make([]string, 0, len(registered))
seen := make(map[string]struct{}, len(registered))
for _, name := range registered {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
continue
}
if _, dup := seen[trimmed]; dup {
continue
}
if hasAllowed {
if _, ok := allowedSet[trimmed]; !ok {
continue
}
}
if _, blocked := disallowedSet[trimmed]; blocked {
continue
}
if !gate.Allow(trimmed) {
continue
}
seen[trimmed] = struct{}{}
out = append(out, trimmed)
}
return out
}
// SortedCopy 返回排序后的名字副本。需要确定序的调用方(如从 map 序的注册表读
// 工具名)先用它归一,再交给 ResolveToolSurface。
func SortedCopy(names []string) []string {
out := append([]string(nil), names...)
sort.Strings(out)
return out
}