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 }