fix(plugin): make native v2 resources explicit

Problem: Native v2 manifests could silently import Claude compatibility sidecars, including repository instructions and executable hooks.

Root cause: parseNativeV2 applied Claude compatibility discovery after validating the explicit manifest.

Fix: Treat the native v2 manifest as the complete capability boundary while preserving compatibility discovery for Codex and Claude manifests. Document the contract and cover it with a regression test.

Verification: go test ./...; cd desktop && go test . -run "Plugin|Theme" -count=1.
This commit is contained in:
SivanCola committed 2026-08-10 00:08:02 +08:00
1 parent 0bf2002f42
commit bd97fecc5b
4 files changed
+61 -8

No files matched your search

+9 -2
View File
@@ -305,6 +305,11 @@ Parsing rules:
- v2 is strict: any unknown field — at the root or nested under
`contributes`/`runtime` — is an error naming the field path, so typos fail
loudly instead of silently disabling a capability.
- v2 resource discovery is explicit. Reasonix loads only the skills, agents,
commands, prompts, hooks, MCP servers, themes, and runtime declared by the
native manifest. Host-specific sidecars such as a root `CLAUDE.md`,
`hooks/hooks.json`, `.claude/settings.json`, or `.mcp.json` are not imported
implicitly.
- Minor aliases (`reasonix.io/plugin/v2.0`, `v2.1`, …) and unknown major
versions are rejected.
- `requires` and `provides` declare dependency constraints and the capability
@@ -387,8 +392,10 @@ before anything is written. Set the optional install name to a marketplace
plugin name to select only that entry. Object sources are accepted only for a
GitHub repository URL pinned to a full commit SHA. Unpinned external strings,
npm, `strict: false`, and other advanced marketplace protocols are skipped in
a bulk install and rejected when selected by name. For packages
such as Superpowers and Claude-style skill packs, Reasonix maps:
a bulk install and rejected when selected by name. For packages such as
Superpowers and Claude-style skill packs, Reasonix maps the following
compatibility conventions. Native v2 manifests use only their explicit
declarations and do not apply these fallbacks:
- `skills` to Reasonix skill roots. A Claude manifest that declares no
`skills` field falls back to the conventional `skills/` (or `.claude/skills/`)
+6 -1
View File
@@ -279,6 +279,10 @@ Reasonix 原生扩展使用精确的 v2 `apiVersion`:
v1 与缺失版本都会被拒绝;不提供 v1 双读或自动迁移路径。
- v2 是严格的:根对象或 `contributes`/`runtime` 下的任何未知字段都会
报错并指明字段路径,避免拼写错误静默失效。
- v2 的资源发现是显式的。Reasonix 只加载原生 manifest 中声明的 skills、
agents、commands、prompts、hooks、MCP servers、themes 与 runtime;不会隐式
导入根目录 `CLAUDE.md`、`hooks/hooks.json`、`.claude/settings.json` 或
`.mcp.json` 等宿主专用 sidecar。
- minor 别名(如 `reasonix.io/plugin/v2.0`、`v2.1`)及未知 major version
都会被拒绝。
- `requires` 与 `provides` 声明依赖约束和能力上限;Sidecar handshake
@@ -346,7 +350,8 @@ Reasonix 的对应实现,并不代表导入 Hook 的每一种运行时决策
中的同名插件。对象来源仅接受 GitHub 仓库 URL 加完整 commit SHA;未固定版本的
外部字符串、npm、`strict: false` 以及其他高级 marketplace 协议在整库安装时会
跳过,按名称选中时则直接报错。
对于 Superpowers 和 Claude 风格 skill 包,Reasonix 会映射:
对于 Superpowers 和 Claude 风格 skill 包,Reasonix 会映射以下兼容约定。
原生 v2 manifest 只使用显式声明,不应用这些回退规则:
- `skills` 到 Reasonix skill root。Claude 清单若未声明 `skills` 字段,会回退到
约定目录 `skills/`(或 `.claude/skills/`),与 Claude 自身的自动发现一致。
+2 -5
View File
@@ -198,17 +198,14 @@ func parseNativeV2(b []byte, root, apiVersion string) (Package, []string, error)
if err := validateV2Capabilities(&manifest); err != nil {
return Package{}, nil, err
}
warnings, issues := applyClaudeCompatibility(root, &manifest)
if err := validateManifest(root, &manifest); err != nil {
return Package{}, warnings, err
}
var warnings []string
pathWarnings, err := validateV1Paths(root, &manifest)
warnings = append(warnings, pathWarnings...)
if err != nil {
return Package{}, warnings, err
}
pkg := Package{Root: root, ManifestKind: "reasonix", Manifest: manifest}
pkg.Compatibility = compatibilityFor(pkg, issues)
pkg.Compatibility = compatibilityFor(pkg, nil)
return pkg, warnings, nil
}
+44
View File
@@ -59,6 +59,50 @@ func TestManifestV2ProviderRequiresSchemaHash(t *testing.T) {
}
}
func TestManifestV2LoadsOnlyExplicitlyDeclaredResources(t *testing.T) {
root := t.TempDir()
writeV2Plugin(t, root, `{
"apiVersion": "reasonix.io/plugin/v2",
"name": "explicit-only",
"contributes": {
"skills": ["skills"],
"commands": ["commands"],
"agents": ["agents"]
}
}`)
writeTestFile(t, filepath.Join(root, "skills", "plan", "SKILL.md"), "---\ndescription: plan work\n---\nPlan carefully.")
writeTestFile(t, filepath.Join(root, "commands", "spec.md"), "---\ndescription: write a spec\n---\nWrite a spec.")
writeTestFile(t, filepath.Join(root, "agents", "reviewer.md"), "---\nname: reviewer\ndescription: review changes\n---\nReview carefully.")
// These Claude compatibility sidecars are present in many multi-host
// packages, but a native v2 manifest is an explicit capability boundary.
writeTestFile(t, filepath.Join(root, "CLAUDE.md"), "Repository maintenance instructions.")
writeTestFile(t, filepath.Join(root, "hooks", "hooks.json"), `{
"hooks": {"SessionStart": [{"hooks": [{"type": "command", "command": "bin/start"}]}]}
}`)
writeTestFile(t, filepath.Join(root, ".mcp.json"), `{
"mcpServers": {"helper": {"command": "bin/helper"}}
}`)
pkg, warnings, err := ParseDir(root)
if err != nil {
t.Fatalf("ParseDir: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("warnings = %v, want none", warnings)
}
inv := pkg.Inventory()
if len(inv.Skills) != 1 || len(inv.Commands) != 1 || len(inv.Agents) != 1 {
t.Fatalf("inventory skills/commands/agents = %d/%d/%d, want 1/1/1", len(inv.Skills), len(inv.Commands), len(inv.Agents))
}
if len(pkg.Manifest.Hooks) != 0 {
t.Fatalf("hooks = %+v, want only explicitly declared resources", pkg.Manifest.Hooks)
}
if len(pkg.Manifest.MCPServers) != 0 {
t.Fatalf("MCP servers = %+v, want only explicitly declared resources", pkg.Manifest.MCPServers)
}
}
func TestMigrateLegacyManifestToV2(t *testing.T) {
root := t.TempDir()
legacy := `{