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 <cursoragent@cursor.com>
This commit is contained in:
@@ -8,35 +8,44 @@ import "encoding/json"
|
|||||||
const PluginAPIVersion = "1.0"
|
const PluginAPIVersion = "1.0"
|
||||||
|
|
||||||
type PluginManifest struct {
|
type PluginManifest struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
APIVersion string `json:"apiVersion"`
|
APIVersion string `json:"apiVersion"`
|
||||||
Requires PluginRequires `json:"requires,omitempty"`
|
Requires PluginRequires `json:"requires,omitempty"`
|
||||||
Provides PluginProvides `json:"provides"`
|
Provides PluginProvides `json:"provides"`
|
||||||
|
Scope PluginScope `json:"scope,omitzero"`
|
||||||
|
Backend PluginBackend `json:"backend,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginRequires struct {
|
type PluginRequires struct {
|
||||||
Network []string `json:"network,omitempty"` // 允许访问的域名(展示用,v1 自装自审)
|
Network []string `json:"network,omitempty"` // 出站域名白名单;v2 空名单拒绝出站,v1 空名单保持放行
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginProvides struct {
|
type PluginProvides struct {
|
||||||
Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名
|
Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名
|
||||||
Workflows []string `json:"workflows,omitempty"`
|
Workflows []string `json:"workflows,omitempty"`
|
||||||
Intents []string `json:"intents,omitempty"`
|
Intents []string `json:"intents,omitempty"`
|
||||||
Prompts []string `json:"prompts,omitempty"`
|
Prompts []string `json:"prompts,omitempty"`
|
||||||
Panels []string `json:"panels,omitempty"`
|
Panels []string `json:"panels,omitempty"`
|
||||||
Cards []string `json:"cards,omitempty"`
|
Cards []string `json:"cards,omitempty"`
|
||||||
Settings []string `json:"settings,omitempty"`
|
Settings []string `json:"settings,omitempty"`
|
||||||
Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库)
|
Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库)
|
||||||
Workspaces []string `json:"workspaces,omitempty"` // workspaces/ 资产名(工作区弹层)
|
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 静态骨架 + 可选数据提供者)。
|
// PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。
|
||||||
type PluginPanel struct {
|
type PluginPanel struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Body []UINode `json:"body"`
|
Body []UINode `json:"body"`
|
||||||
// Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。
|
// Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。
|
||||||
RefreshTool string `json:"refreshTool,omitempty"`
|
RefreshTool string `json:"refreshTool,omitempty"`
|
||||||
@@ -44,23 +53,23 @@ type PluginPanel struct {
|
|||||||
|
|
||||||
// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。
|
// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。
|
||||||
type WorkflowDef struct {
|
type WorkflowDef struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动)
|
IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动)
|
||||||
Steps []WorkflowStep `json:"steps"`
|
Steps []WorkflowStep `json:"steps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowStep struct {
|
type WorkflowStep struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card
|
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card
|
||||||
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
|
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
|
||||||
Tool string `json:"tool,omitempty"` // tool: 工具名
|
Tool string `json:"tool,omitempty"` // tool: 工具名
|
||||||
Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板
|
Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板
|
||||||
Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板
|
Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板
|
||||||
Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板
|
Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板
|
||||||
Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡
|
Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成
|
// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成
|
||||||
@@ -76,20 +85,20 @@ type ActivityCardTemplate struct {
|
|||||||
// PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场,
|
// PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场,
|
||||||
// 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。
|
// 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。
|
||||||
type PluginWorkspace struct {
|
type PluginWorkspace struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出)
|
Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出)
|
||||||
BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone}
|
BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone}
|
||||||
Body []UINode `json:"body"`
|
Body []UINode `json:"body"`
|
||||||
// RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。
|
// RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。
|
||||||
RefreshTool string `json:"refreshTool,omitempty"`
|
RefreshTool string `json:"refreshTool,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HITLCardTemplate struct {
|
type HITLCardTemplate struct {
|
||||||
Question string `json:"question"`
|
Question string `json:"question"`
|
||||||
Kind HITLKind `json:"kind"`
|
Kind HITLKind `json:"kind"`
|
||||||
Choices []HITLChoice `json:"choices,omitempty"`
|
Choices []HITLChoice `json:"choices,omitempty"`
|
||||||
AllowFreeInput bool `json:"allowFreeInput"`
|
AllowFreeInput bool `json:"allowFreeInput"`
|
||||||
FreeInputHint string `json:"freeInputHint,omitempty"`
|
FreeInputHint string `json:"freeInputHint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
+100
@@ -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/<name>.md 的装载结果(SKILL.md 正文)。
|
||||||
|
type PluginSkill struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMCPServer 是 mcpServers/<name>.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"`
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -248,7 +248,11 @@ export interface PluginManifest {
|
|||||||
tools?: string[]; workflows?: string[]; intents?: string[];
|
tools?: string[]; workflows?: string[]; intents?: string[];
|
||||||
prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[];
|
prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[];
|
||||||
commands?: string[]; workspaces?: 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 {
|
export interface PluginPanel {
|
||||||
@@ -260,6 +264,10 @@ export interface PluginWorkspace {
|
|||||||
badgeTool?: string; body: UINode[]; refreshTool?: string;
|
badgeTool?: string; body: UINode[]; refreshTool?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginPageDecl {
|
||||||
|
id: string; title: string; module: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 摄取 / 检索 ----
|
// ---- 摄取 / 检索 ----
|
||||||
|
|
||||||
export interface ParsedDocument {
|
export interface ParsedDocument {
|
||||||
|
|||||||
Reference in New Issue
Block a user