mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
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:
1 parent
0bf2002f42
commit
bd97fecc5b
4 files changed
+61
-8
No files matched your search
@@ -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/`)
|
||||
|
||||
@@ -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 自身的自动发现一致。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 := `{
|
||||
|
||||
Reference in new issue
Block a user