From fdc0c1b933abc0f2025b71fa9da33625be288fde Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 1 Oct 2026 10:34:29 +0000 Subject: [PATCH] Accept plugin manifest apiVersion 2.0-experimental Why: Downstream repositories need a versioned public manifest for skills, MCP, knowledge, parsers, agents, channels, and pages without forking the base. Evidence: go test ./... passed after omitzero on empty scope and backend so the golden plugin_manifest stays stable. Impact: apiVersion 1.0 remains accepted. New provides fields are optional. Scope and backend are omitted when zero. Refs: docs/proposals/2026-10-01-plugin-contract-v2.md Co-authored-by: Cursor --- plugin.go | 95 +++++++++++++++++++++++-------------------- plugin_v2.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++ plugin_v2_test.go | 26 ++++++++++++ ts/types.ts | 8 ++++ 4 files changed, 186 insertions(+), 43 deletions(-) create mode 100644 plugin_v2.go create mode 100644 plugin_v2_test.go diff --git a/plugin.go b/plugin.go index a610c9b..708deb7 100644 --- a/plugin.go +++ b/plugin.go @@ -8,35 +8,44 @@ import "encoding/json" const PluginAPIVersion = "1.0" type PluginManifest struct { - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - APIVersion string `json:"apiVersion"` - Requires PluginRequires `json:"requires,omitempty"` - Provides PluginProvides `json:"provides"` + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + Requires PluginRequires `json:"requires,omitempty"` + Provides PluginProvides `json:"provides"` + Scope PluginScope `json:"scope,omitzero"` + Backend PluginBackend `json:"backend,omitzero"` } type PluginRequires struct { - Network []string `json:"network,omitempty"` // 允许访问的域名(展示用,v1 自装自审) + Network []string `json:"network,omitempty"` // 出站域名白名单;v2 空名单拒绝出站,v1 空名单保持放行 } type PluginProvides struct { - Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名 - Workflows []string `json:"workflows,omitempty"` - Intents []string `json:"intents,omitempty"` - Prompts []string `json:"prompts,omitempty"` - Panels []string `json:"panels,omitempty"` - Cards []string `json:"cards,omitempty"` - Settings []string `json:"settings,omitempty"` - Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库) + Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名 + Workflows []string `json:"workflows,omitempty"` + Intents []string `json:"intents,omitempty"` + Prompts []string `json:"prompts,omitempty"` + Panels []string `json:"panels,omitempty"` + Cards []string `json:"cards,omitempty"` + Settings []string `json:"settings,omitempty"` + Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库) Workspaces []string `json:"workspaces,omitempty"` // workspaces/ 资产名(工作区弹层) + Skills []string `json:"skills,omitempty"` + MCPServers []string `json:"mcpServers,omitempty"` + Knowledge []string `json:"knowledge,omitempty"` + Parsers []string `json:"parsers,omitempty"` + Agents []string `json:"agents,omitempty"` + Channels []string `json:"channels,omitempty"` + Pages []string `json:"pages,omitempty"` } // PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。 type PluginPanel struct { - ID string `json:"id"` - Title string `json:"title"` - Icon string `json:"icon,omitempty"` + ID string `json:"id"` + Title string `json:"title"` + Icon string `json:"icon,omitempty"` Body []UINode `json:"body"` // Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。 RefreshTool string `json:"refreshTool,omitempty"` @@ -44,23 +53,23 @@ type PluginPanel struct { // WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。 type WorkflowDef struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动) - Steps []WorkflowStep `json:"steps"` + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动) + Steps []WorkflowStep `json:"steps"` } type WorkflowStep struct { - ID string `json:"id"` - Title string `json:"title"` - Kind string `json:"kind"` // prompt | tool | hitl | artifact | card - Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板 - Tool string `json:"tool,omitempty"` // tool: 工具名 - Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板 - Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板 - Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板 - Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡 + ID string `json:"id"` + Title string `json:"title"` + Kind string `json:"kind"` // prompt | tool | hitl | artifact | card + Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板 + Tool string `json:"tool,omitempty"` // tool: 工具名 + Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板 + Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板 + Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板 + Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡 } // ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成 @@ -76,20 +85,20 @@ type ActivityCardTemplate struct { // PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场, // 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。 type PluginWorkspace struct { - ID string `json:"id"` - Title string `json:"title"` - Icon string `json:"icon,omitempty"` - Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出) - BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone} - Body []UINode `json:"body"` + ID string `json:"id"` + Title string `json:"title"` + Icon string `json:"icon,omitempty"` + Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出) + BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone} + Body []UINode `json:"body"` // RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。 RefreshTool string `json:"refreshTool,omitempty"` } type HITLCardTemplate struct { - Question string `json:"question"` - Kind HITLKind `json:"kind"` - Choices []HITLChoice `json:"choices,omitempty"` - AllowFreeInput bool `json:"allowFreeInput"` - FreeInputHint string `json:"freeInputHint,omitempty"` + Question string `json:"question"` + Kind HITLKind `json:"kind"` + Choices []HITLChoice `json:"choices,omitempty"` + AllowFreeInput bool `json:"allowFreeInput"` + FreeInputHint string `json:"freeInputHint,omitempty"` } diff --git a/plugin_v2.go b/plugin_v2.go new file mode 100644 index 0000000..d1be278 --- /dev/null +++ b/plugin_v2.go @@ -0,0 +1,100 @@ +package core + +// PluginAPIVersionExperimental 是插件契约 v2 的实验窗口。 +// 1.0 清单继续有效;v2 只增加可选字段,不收紧 1.0 已有资产。 +const PluginAPIVersionExperimental = "2.0-experimental" + +// PluginAPIAccepted 报告宿主是否接受该 apiVersion。空值由宿主填成 PluginAPIVersion。 +func PluginAPIAccepted(v string) bool { + switch v { + case PluginAPIVersion, PluginAPIVersionExperimental: + return true + default: + return false + } +} + +// PluginScope 限制插件对哪些租户/项目可见。两者都空 = 全部可见。 +type PluginScope struct { + Tenants []string `json:"tenants,omitempty"` + Projects []string `json:"projects,omitempty"` +} + +// Allows 判断 owner/project 是否落在作用域内。 +func (s PluginScope) Allows(owner, project string) bool { + if len(s.Tenants) > 0 && !scopeContains(s.Tenants, owner) { + return false + } + if len(s.Projects) > 0 && !scopeContains(s.Projects, project) { + return false + } + return true +} + +func scopeContains(xs []string, v string) bool { + for _, x := range xs { + if x == v { + return true + } + } + return false +} + +// PluginBackend 声明进程内或 sidecar 代码档。SchemaVersion 是插件自有数据的迁移版本。 +type PluginBackend struct { + Mode string `json:"mode,omitempty"` // inproc | sidecar + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + SchemaVersion int `json:"schemaVersion,omitempty"` +} + +// PluginSkill 是 skills/.md 的装载结果(SKILL.md 正文)。 +type PluginSkill struct { + Name string `json:"name"` + Body string `json:"body"` +} + +// PluginMCPServer 是 mcpServers/.json,字段与 agent-native MCP ServerConfig 对齐。 +type PluginMCPServer struct { + Name string `json:"name"` + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + URL string `json:"url,omitempty"` +} + +// PluginKnowledgeSet 是知识数据集声明(数据在插件侧,检索引擎在基座)。 +type PluginKnowledgeSet struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description,omitempty"` +} + +// PluginParserDecl 是解析器声明。实现可以是内置名或 sidecar 工具名。 +type PluginParserDecl struct { + ID string `json:"id"` + Title string `json:"title"` + MIME string `json:"mime,omitempty"` + Tool string `json:"tool,omitempty"` +} + +// PluginAgentDecl 是画像/子代理声明。 +type PluginAgentDecl struct { + ID string `json:"id"` + Title string `json:"title"` + Instructions string `json:"instructions,omitempty"` + Tools []string `json:"tools,omitempty"` +} + +// PluginChannelDecl 是渠道适配器声明(飞书等由下游实现,基座只登记入口)。 +type PluginChannelDecl struct { + ID string `json:"id"` + Kind string `json:"kind"` + Title string `json:"title,omitempty"` +} + +// PluginPageDecl 是前端 ESM 页面。Module 相对插件目录或绝对 URL。 +type PluginPageDecl struct { + ID string `json:"id"` + Title string `json:"title"` + Module string `json:"module"` +} diff --git a/plugin_v2_test.go b/plugin_v2_test.go new file mode 100644 index 0000000..41b7950 --- /dev/null +++ b/plugin_v2_test.go @@ -0,0 +1,26 @@ +package core + +import "testing" + +func TestPluginAPIAccepted(t *testing.T) { + if !PluginAPIAccepted(PluginAPIVersion) || !PluginAPIAccepted(PluginAPIVersionExperimental) { + t.Fatal("current and experimental windows must be accepted") + } + if PluginAPIAccepted("0.9") || PluginAPIAccepted("") { + t.Fatal("unknown or empty apiVersion must be rejected") + } +} + +func TestPluginScopeAllows(t *testing.T) { + open := PluginScope{} + if !open.Allows("a", "p") { + t.Fatal("empty scope is visible to everyone") + } + scoped := PluginScope{Tenants: []string{"acme"}, Projects: []string{"p1"}} + if !scoped.Allows("acme", "p1") { + t.Fatal("matching tenant and project should pass") + } + if scoped.Allows("other", "p1") || scoped.Allows("acme", "p2") { + t.Fatal("scope must reject mismatches") + } +} diff --git a/ts/types.ts b/ts/types.ts index e127d6a..85f3dfd 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -248,7 +248,11 @@ export interface PluginManifest { tools?: string[]; workflows?: string[]; intents?: string[]; prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[]; commands?: string[]; workspaces?: string[]; + skills?: string[]; mcpServers?: string[]; knowledge?: string[]; + parsers?: string[]; agents?: string[]; channels?: string[]; pages?: string[]; }; + scope?: { tenants?: string[]; projects?: string[] }; + backend?: { mode?: 'inproc' | 'sidecar'; command?: string; args?: string[]; schemaVersion?: number }; } export interface PluginPanel { @@ -260,6 +264,10 @@ export interface PluginWorkspace { badgeTool?: string; body: UINode[]; refreshTool?: string; } +export interface PluginPageDecl { + id: string; title: string; module: string; +} + // ---- 摄取 / 检索 ---- export interface ParsedDocument {