diff --git a/deps/agent-native b/deps/agent-native index 77e7f97..7425b32 160000 --- a/deps/agent-native +++ b/deps/agent-native @@ -1 +1 @@ -Subproject commit 77e7f97827d37ec0bdc0323dd30ac65a68e4aa5d +Subproject commit 7425b32b9ebd7050f3630f3c0de8a3ade28c1606 diff --git a/deps/agent-squad b/deps/agent-squad index 92f4e92..9d3faef 160000 --- a/deps/agent-squad +++ b/deps/agent-squad @@ -1 +1 @@ -Subproject commit 92f4e925d2920efb284044ccfff17102666ca454 +Subproject commit 9d3faefb97a6ce8e83c143f41fcc87e4631d1c92 diff --git a/internal/plugins/host_v2.go b/internal/plugins/host_v2.go index 6b5c02e..b02b827 100644 --- a/internal/plugins/host_v2.go +++ b/internal/plugins/host_v2.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "agent-system-backend/internal/skills" "agent-system-backend/internal/store" "agent-system-backend/internal/tools" core "agent-system-core" @@ -59,6 +60,10 @@ func (h *Host) loadDeclared(dir string, m core.PluginManifest, ld *Loaded) error if err != nil { return fmt.Errorf("plugins: %s: skill %s: %w", m.ID, name, err) } + // 技能按开放规范校验(N2 WP4):不合规整批拒载,字段定位到键。 + if err := skills.ValidateForPlugin(m.ID, name, body); err != nil { + return err + } if ld.Skills == nil { ld.Skills = map[string]string{} } diff --git a/internal/plugins/host_v2_test.go b/internal/plugins/host_v2_test.go index 80f65ce..c22778f 100644 --- a/internal/plugins/host_v2_test.go +++ b/internal/plugins/host_v2_test.go @@ -2,6 +2,7 @@ package plugins import ( "os" + "strings" "path/filepath" "testing" "time" @@ -33,7 +34,7 @@ func TestV2AssetsAndNetwork(t *testing.T) { "provides":{"tools":["echo"],"skills":["note"],"mcpServers":["local"],"knowledge":["regs"],"parsers":["pdf"],"agents":["writer"],"channels":["feishu"],"pages":["home"]} }`) write("tools/echo.json", `{"name":"v2_echo","description":"e","parameters":{"type":"object"},"http":{"method":"GET","url":"https://api.example.com/x"}}`) - write("skills/note.md", "# note\n") + write("skills/note.md", "---\nname: note\ndescription: 笔记技能\n---\n\n# note\n") write("mcp/local.json", `{"name":"local","command":"echo"}`) write("knowledge/regs.json", `{"id":"regs","title":"法规"}`) write("parsers/pdf.json", `{"id":"pdf","title":"PDF","mime":"application/pdf"}`) @@ -95,3 +96,33 @@ func TestSidecarTouch(t *testing.T) { } t.Fatal("sidecar did not run") } + +// N2 WP4:不合规插件技能整批拒载,错误带插件上下文与字段定位。 +func TestV2InvalidSkillRejected(t *testing.T) { + host, _, dir := newHost(t) + root := filepath.Join(dir, "badskill") + if err := os.MkdirAll(filepath.Join(root, "skills"), 0o755); err != nil { + t.Fatal(err) + } + write := func(rel, body string) { + t.Helper() + if err := os.WriteFile(filepath.Join(root, rel), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + write("plugin.json", `{ + "id":"badskill","name":"Bad","version":"0.1.0","apiVersion":"`+core.PluginAPIVersionExperimental+`", + "backend":{"schemaVersion":1}, + "provides":{"skills":["note"]} + }`) + // 缺 name(开放规范必填)。 + write("skills/note.md", "---\ndescription: 没有名字\n---\n正文\n") + err := host.LoadDir()["badskill"] + if err == nil { + t.Fatal("不合规技能必须拒载") + } + if !strings.Contains(err.Error(), "plugins: badskill: skill note:") || + !strings.Contains(err.Error(), "name") { + t.Fatalf("错误须带插件上下文与字段定位:%v", err) + } +} diff --git a/internal/skills/recipes/docx-author/SKILL.md b/internal/skills/recipes/docx-author/SKILL.md index 4cdafff..91836ff 100644 --- a/internal/skills/recipes/docx-author/SKILL.md +++ b/internal/skills/recipes/docx-author/SKILL.md @@ -1,4 +1,5 @@ --- +name: docx-author description: 用 housestyle 生成可直接交付的 Word(.docx) --- diff --git a/internal/skills/recipes/house-style/SKILL.md b/internal/skills/recipes/house-style/SKILL.md index 91053b6..1d34897 100644 --- a/internal/skills/recipes/house-style/SKILL.md +++ b/internal/skills/recipes/house-style/SKILL.md @@ -1,4 +1,5 @@ --- +name: house-style description: 交付物设计系统。用 housestyle 生成 Word、Excel、PowerPoint,并在登记前自检。 --- diff --git a/internal/skills/recipes/pptx-author/SKILL.md b/internal/skills/recipes/pptx-author/SKILL.md index 7593279..377d4d1 100644 --- a/internal/skills/recipes/pptx-author/SKILL.md +++ b/internal/skills/recipes/pptx-author/SKILL.md @@ -1,4 +1,5 @@ --- +name: pptx-author description: 用 housestyle 生成 16:9 的 PowerPoint(.pptx) --- diff --git a/internal/skills/recipes/xlsx-author/SKILL.md b/internal/skills/recipes/xlsx-author/SKILL.md index e6ac495..e99439e 100644 --- a/internal/skills/recipes/xlsx-author/SKILL.md +++ b/internal/skills/recipes/xlsx-author/SKILL.md @@ -1,4 +1,5 @@ --- +name: xlsx-author description: 用 housestyle 生成带表头、公式和说明页的 Excel(.xlsx) --- diff --git a/internal/skills/skills.go b/internal/skills/skills.go index 001025b..d6e3511 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -12,6 +12,7 @@ import ( "embed" "encoding/hex" "encoding/json" + "fmt" "io/fs" "os" "path/filepath" @@ -80,6 +81,14 @@ func Seed(dir string) (SeedResult, error) { if err != nil { return err } + // SKILL.md 种子写入前过开放规范校验(recipes 全 dogfood 合规; + // 不合规拒写并上浮——错误不得静默落盘)。 + if d.Name() == "SKILL.md" { + skillName := filepath.Base(filepath.Dir(rel)) + if verr := validateBuiltin(skillName, data); verr != nil { + return fmt.Errorf("builtin skill %s: %w", skillName, verr) + } + } embedded := hashBytes(data) dst := filepath.Join(dir, filepath.FromSlash(rel)) disk, err := os.ReadFile(dst) diff --git a/internal/skills/validate.go b/internal/skills/validate.go new file mode 100644 index 0000000..d0f978d --- /dev/null +++ b/internal/skills/validate.go @@ -0,0 +1,23 @@ +package skills + +import ( + "fmt" + + agentskills "agent-native/runtime/skills" +) + +// ValidateForPlugin 用 agent-native 的同一校验器验证插件技能 +// (开放规范:name 必填且与目录同名、description 必填、已知键白名单)。 +// 失败带插件上下文与字段定位,由调用方整批拒绝该插件(fail-closed, +// 与 agents/ 资产的禁带键同款形态)。 +func ValidateForPlugin(pluginID, name string, body []byte) error { + if err := agentskills.ValidateSkillDocument(name, "plugins/"+pluginID+"/skills/"+name+".md", body); err != nil { + return fmt.Errorf("plugins: %s: skill %s: %w", pluginID, name, err) + } + return nil +} + +// validateBuiltin 校验内置配方(种子写入前置;recipes 全部 dogfood 合规)。 +func validateBuiltin(name string, body []byte) error { + return agentskills.ValidateSkillDocument(name, "recipes/"+name+"/SKILL.md", body) +}