Validate plugin and bundled skills against the open spec (N2 WP4) #89

Merged
weichen merged 2 commits from feature/n2-skills-wp4 into main 2026-10-07 13:58:47 +00:00
10 changed files with 75 additions and 3 deletions
+5
View File
@@ -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{}
}
+32 -1
View File
@@ -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)
}
}
@@ -1,4 +1,5 @@
---
name: docx-author
description: 用 housestyle 生成可直接交付的 Word(.docx)
---
@@ -1,4 +1,5 @@
---
name: house-style
description: 交付物设计系统。用 housestyle 生成 Word、Excel、PowerPoint,并在登记前自检。
---
@@ -1,4 +1,5 @@
---
name: pptx-author
description: 用 housestyle 生成 16:9 的 PowerPoint(.pptx)
---
@@ -1,4 +1,5 @@
---
name: xlsx-author
description: 用 housestyle 生成带表头、公式和说明页的 Excel(.xlsx)
---
+9
View File
@@ -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)
+23
View File
@@ -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)
}