Validate plugin and bundled skills against the open spec (N2 WP4) #89
Vendored
+1
-1
Submodule deps/agent-native updated: 77e7f97827...7425b32b9e
Vendored
+1
-1
Submodule deps/agent-squad updated: 92f4e925d2...9d3faefb97
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user