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 的分阶段)。
This commit is contained in:
1 parent
29181245ce
commit
f75e7a9c9b
17 files changed
+3067
-164
No files matched your search
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user