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,150 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 本文件专测 design/09 §6.4-3 的能力收缩:**只收不放**。
|
||||
|
||||
func TestResolveToolSurface_表驱动(t *testing.T) {
|
||||
t.Parallel()
|
||||
registered := []string{"alpha", "beta", "gamma"}
|
||||
cases := []struct {
|
||||
name string
|
||||
allowed []string
|
||||
disallowed []string
|
||||
gate Gate
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "未声明上限时不额外收窄(仍受安全闸约束)",
|
||||
gate: NewNameSet([]string{"alpha", "beta", "gamma"}),
|
||||
want: "alpha,beta,gamma",
|
||||
},
|
||||
{
|
||||
name: "allowed 是上限:列表外的工具不可见",
|
||||
allowed: []string{"alpha"},
|
||||
gate: NewNameSet([]string{"alpha", "beta", "gamma"}),
|
||||
want: "alpha",
|
||||
},
|
||||
{
|
||||
name: "disallowed 从上限里剔除",
|
||||
allowed: []string{"alpha", "beta"},
|
||||
disallowed: []string{"beta"},
|
||||
gate: NewNameSet([]string{"alpha", "beta", "gamma"}),
|
||||
want: "alpha",
|
||||
},
|
||||
{
|
||||
name: "安全闸拒绝的工具即使被技能允许也不可用",
|
||||
allowed: []string{"alpha", "beta"},
|
||||
gate: NewNameSet([]string{"alpha"}),
|
||||
want: "alpha",
|
||||
},
|
||||
{
|
||||
name: "allowed 里的幽灵工具不会凭空出现(∩ 注册表)",
|
||||
allowed: []string{"alpha", "ghost"},
|
||||
gate: NewNameSet([]string{"alpha", "ghost"}),
|
||||
want: "alpha",
|
||||
},
|
||||
{
|
||||
name: "缺安全闸 ⇒ 全拒(fail-closed)",
|
||||
gate: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "空注册表 ⇒ 空面",
|
||||
gate: NewNameSet([]string{"alpha"}),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tools := registered
|
||||
if tc.name == "空注册表 ⇒ 空面" {
|
||||
tools = nil
|
||||
}
|
||||
got := ResolveToolSurface(tools, tc.allowed, tc.disallowed, tc.gate)
|
||||
if strings.Join(got, ",") != tc.want {
|
||||
t.Fatalf("ResolveToolSurface = %q, want %q", strings.Join(got, ","), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveToolSurface_只收不放 是本片的核心不变式:无论技能怎么声明,结果永远
|
||||
// 是「注册表 ∩ 安全闸」的子集 —— 技能**加不进**任何东西。
|
||||
func TestResolveToolSurface_只收不放(t *testing.T) {
|
||||
t.Parallel()
|
||||
registered := []string{"alpha", "beta"}
|
||||
gate := NewNameSet([]string{"alpha", "beta"})
|
||||
// 技能把 allowed-tools 写成注册表之外的、或安全闸之外的,都不该出现在结果里。
|
||||
got := ResolveToolSurface(registered, []string{"ghost", "secret"}, nil, gate)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("ResolveToolSurface = %v, want 空(技能不得放宽任何工具)", got)
|
||||
}
|
||||
// 反过来:技能把*所有*注册表工具都写进 allowed,也只能拿到安全闸允许的那些。
|
||||
got = ResolveToolSurface(registered, registered, nil, NewNameSet([]string{"beta"}))
|
||||
if strings.Join(got, ",") != "beta" {
|
||||
t.Fatalf("ResolveToolSurface = %v, want [beta]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveToolSurface_去重与保序(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := ResolveToolSurface([]string{"b", "a", "b", " ", "c"}, nil, nil, NewNameSet([]string{"a", "b", "c"}))
|
||||
if strings.Join(got, ",") != "b,a,c" {
|
||||
t.Fatalf("ResolveToolSurface = %q, want b,a,c(保注册表序 + 去重 + 跳空白)", strings.Join(got, ","))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllOf_全允许才允许(t *testing.T) {
|
||||
t.Parallel()
|
||||
allowAll := NewNameSet([]string{"a", "b"})
|
||||
onlyA := NewNameSet([]string{"a"})
|
||||
if !AllOf(allowAll, onlyA).Allow("a") {
|
||||
t.Fatal("AllOf(allowAll, onlyA).Allow(a) = false, want true")
|
||||
}
|
||||
if AllOf(allowAll, onlyA).Allow("b") {
|
||||
t.Fatal("AllOf(allowAll, onlyA).Allow(b) = true, want false")
|
||||
}
|
||||
if AllOf(allowAll, GateFunc(nil)).Allow("a") {
|
||||
t.Fatal("含 nil 函数闸时应当全拒")
|
||||
}
|
||||
// 没有任何有效闸 ⇒ 全拒(绝不给出无人看守的工具面)。
|
||||
if AllOf(nil, nil).Allow("a") {
|
||||
t.Fatal("AllOf() 全 nil 时应当全拒")
|
||||
}
|
||||
if AllOf().Allow("a") {
|
||||
t.Fatal("AllOf() 无参时应当全拒")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameSet_语义(t *testing.T) {
|
||||
t.Parallel()
|
||||
set := NewNameSet([]string{" a ", "", "b"})
|
||||
if !set.Allow("a") || !set.Allow("b") {
|
||||
t.Fatalf("NameSet = %v, want 含 a/b(并去掉空白)", set)
|
||||
}
|
||||
if set.Allow("") || set.Allow("c") {
|
||||
t.Fatal("NameSet 不该放行未登记的名字")
|
||||
}
|
||||
var nilSet NameSet
|
||||
if nilSet.Allow("a") {
|
||||
t.Fatal("nil NameSet 必须全拒")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortedCopy_不改原切片(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := []string{"b", "a"}
|
||||
got := SortedCopy(in)
|
||||
if strings.Join(got, ",") != "a,b" {
|
||||
t.Fatalf("SortedCopy = %v, want [a b]", got)
|
||||
}
|
||||
if in[0] != "b" {
|
||||
t.Fatalf("SortedCopy 改动了入参:%v", in)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user