- 加载器/校验:目录式 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 的分阶段)。
136 lines
4.4 KiB
Go
136 lines
4.4 KiB
Go
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
|
||
}
|