02. 插件Manifest Schema
翻译说明: 本页是与 英文源规格 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。
1. 目的
冻结插件清单字段以保证:
- 主机可以验证
- 开发人员可以依赖它
- 未来版本可以迁移
架构版本:1
2. 根对象
type PluginManifestV1 = {
schemaVersion: 1;
id: string; // ^[a-z0-9]+(\.[a-z0-9_-]+)+$
name: string;
version: string; // semver
description?: string;
/**
* 展示文案的多语言表:当应用语言命中其中某个 locale 时用它替换
* `name` / `description`(见 §3.1)。扁平字段仍是作者自己的语言,也是最终回退。
*/
i18n?: {
[locale: string]: {
name?: string;
description?: string;
safetyNotes?: string;
};
};
author?: string | { name: string; url?: string; email?: string };
homepage?: string;
repository?: string;
icon?: string; // relative path
main?: string; // plugin runtime entry
ui?: PluginUiConfig;
contributes?: PluginContributes;
permissions?: PluginPermission[];
fs?: PluginFsPolicy; // 每个文件权限可以触碰哪些路径(§5.2)
net?: { domains?: string[] }; // 出网白名单(§5.3)
engines?: {
piDesktop?: string; // semver range
};
entrypoints?: {
onInstall?: string;
onLoad?: string;
onEnable?: string;
onDisable?: string;
onUnload?: string;
onUninstall?: string;
};
activationEvents?: string[]; // e.g. onCommand:xxx / onStartup
/**
* 随应用打包插件的首次注册默认值。省略表示启用。
* 市场安装和开发加载仍在用户授权后启用。
*/
enabledByDefault?: boolean;
};3. 用户界面配置
type PluginUiConfig = {
panel?: string; // html entry
width?: number;
height?: number;
resizable?: boolean;
title?: string;
};3.1 多语言标签(i18n)
name、description 和 safetyNotes 都是展示文案,插件可以用顶层 i18n 块按 locale 声明。扩展页、插件启动器和市场(从 catalog 条目读取同一个块)显示的是应用 语言对应的条目,而不是作者自己写的那一种:
{
"name": "小清新待办",
"description": "作者原话",
"i18n": {
"en": { "name": "Todo List", "description": "A calm todo list" },
"zh-CN": { "name": "小清新待办", "description": "轻盈的待办清单", "safetyNotes": "只写自己的数据" }
}
}规则:
en与zh-CN是契约 locale。所有中文壳 locale(zh、zh-CN、zh-Hans、zh-SG)读zh-CN,其余读en。插件不必为其他已发布壳 locale 提供翻译, 因此zh-TW读英文,而不是拿半份zh-CN猜测(ADR 0182)。- 插件仓库的校验器要求两个 locale 与三个字段齐全,但宿主是宽容的:缺 locale、 缺字段或空字符串会按字段依次回退到另一个契约 locale,再回退到作者扁平的
name/description。 - 解析发生在宿主里,依据桌面壳下发的语言(
settings.language,为auto时是 系统语言)。存储行保留作者原文,因此切换语言只改变读取结果,绝不改写注册表。 - 该块是展示元数据。格式错误(不是 locale → 对象的对象)会让 manifest 校验失败; 条目里未知的 locale 与未知字段一律忽略。
- 该块只服务身份文案(
name、description、safetyNotes)。插件自有文案——面板、 视图、widget、生成式设置、toast、运行时命令标题——不在这里翻译。宿主只发布当前 语言(pi.app.getLocale、appearance:changed),由插件自行本地化(ADR 0280)。
4. 贡献
type PluginContributes = {
commands?: PluginCommandContrib[];
agentTools?: PluginAgentToolContrib[];
skills?: Array<string | PluginSkillContrib>; // relative paths, or metadata overrides
agentExtensions?: string[]; // 在 agent sidecar 内运行的 ExtensionAPI 模块;需要 `agent.extension`(规格 16)
providers?: PluginProviderContrib[]; // 宿主拥有的 provider 行;需要 `provider.register`(规格 13)
settings?: PluginSettingContrib[];
themes?: PluginThemeContrib[];
windowAppearance?: PluginWindowAppearanceContrib; // 原生窗口背景;需要 `ui.window.appearance`
mcpServers?: PluginMcpServerContrib[];
services?: PluginServiceContrib[];
bus?: PluginBusContrib;
views?: PluginViewContrib[];
sessionSources?: PluginSessionSourceContrib[];
globalShortcuts?: PluginGlobalShortcutContrib[]; // 需要 `keyboard.globalShortcut`
};
type PluginCommandContrib = {
id: string; // plugin-local or fully-qualified
title: string;
keywords?: string[];
category?: string;
icon?: string;
requires?: PluginPermission[]; // extra per-command perms
};
type PluginAgentToolContrib = {
name: string; // tool name exposed to agent
description: string;
risk: "low" | "medium" | "high";
schema: Record<string, unknown>; // JSON schema object
timeoutMs?: number;
permissions?: PluginPermission[];
};
type PluginSettingContrib = {
key: string;
title: string; // 作者语言;生成式设置面板不做本地化
description?: string;
type: "string" | "number" | "boolean" | "select" | "json" | "shortcut";
default?: unknown;
enum?: Array<{ label: string; value: string | number | boolean }>;
command?: string; // shortcut 设置调用这个已声明的插件命令
scope?: "plugin"; // 暂不支持全局插件快捷键
secret?: boolean;
};
type PluginViewContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$,插件内唯一
title: string | { en: string; "zh-CN": string };
icon?: string; // 宿主图标集中的 token;未知 token 渲染为字母瓷砖
entry: string; // 视图 HTML 入口的相对路径
order?: number; // 插件视图分组内的升序排序键,默认 0
};
type PluginSessionSourceContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9._-]{0,63}$,插件内唯一
label?: string | { en: string; "zh-CN": string };
};
/** 插件声明的一个系统级快捷键(`keyboard.globalShortcut`)。 */
type PluginGlobalShortcutContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9._-]{0,63}$,插件内唯一
command: string; // 必须声明在 contributes.commands 里
default?: string; // 宿主在加载后注册的加速键;省略则由 `pi.keyboard` 稍后注册
};
type PluginThemeContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$
label: string;
path: string; // relative `.css` file
base?: "light" | "dark"; // palette the overrides layer on, default `dark`
assets?: string[]; // 绝对路径的 png/jpg/jpeg/webp/avif/svg/woff2,总和上限 4 MB;
// 命中的 `url()` 会被改写为 `plugin-asset://`
};
type PluginWindowAppearanceContrib = {
backgroundColor?: { light?: string; dark?: string }; // #rrggbb | #rrggbbaa
};
type PluginSkillContrib = {
id?: string; // defaults to the file name without its extension
path: string; // relative path to the skill document
name?: string; // overrides the front-matter `name`
description?: string; // overrides the front-matter `description`
};
type PluginMcpServerContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$
label?: string;
transport: "stdio" | "http";
// stdio only
command?: string; // bare PATH name, or plugin-relative executable
args?: string[];
env?: Record<string, string | { setting: string }>;
// 远程 HTTP 传输
url?: string; // 绝对 http(s) 端点;HTTP 可以指向可信的局域网主机
headers?: Record<string, string | { setting: string }>;
};
type PluginServiceContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$
label?: string;
autoRestart?: boolean; // default true
};
type PluginBusContrib = {
publish?: string[]; // concrete topics, e.g. `build.done`
subscribe?: string[]; // patterns, e.g. `build.*` / `build.**`
};
type PluginProviderContrib = {
id: string; // ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$,插件内唯一
name: string; // 原生 provider 列表中的显示名
vendorKey?: string; // models.dev 供应商键,默认 `custom`
baseUrl?: string; // 绝对 http(s) URL
apiStyle?: PluginProviderApiStyle; // 线路风格,默认 `chat_completions`
authKind?: "api_key" | "none"; // 默认 `api_key`;`oauth` 暂被拒绝
models: PluginProviderModelContrib[]; // 1..64 条
};
type PluginProviderApiStyle =
| "chat_completions"
| "opencode_go"
| "responses"
| "anthropic_messages"
| "google_generative_ai"
| "openai_codex_responses"
| "pi_messages";
type PluginProviderModelContrib = {
id: string; // 1..256 个字符,provider 内唯一
name?: string; // 模型绑定的显示标签
contextWindow?: number;
maxTokens?: number;
supportsImages?: boolean;
/** 模型可提供的规范思考档位,按声明顺序保留。 */
thinkingLevels?: string[];
/** 当该值存在于 `thinkingLevels` 时,新会话使用它。 */
defaultThinkingLevel?: string;
};thinkingLevels 可选。宿主会裁剪条目、丢弃未知规范档位、去重,并保留剩余的声明顺序。 缺失或不可用的列表会变成空绑定。只有当 defaultThinkingLevel 命中该模型列表中的归一化档位 时才会保留;否则会被丢弃,普通绑定归一化会选择第一个可用档位。 清单校验会拒绝非数组的 thinkingLevels、其中任何非字符串条目,或非字符串的 defaultThinkingLevel;未知的字符串档位则会被接受并在归一化时丢弃。
5. 权限枚举
type PluginPermission =
| "ui.panel"
| "ui.view"
| "ui.theme"
| "ui.window.appearance"
| "clipboard.read"
| "clipboard.write"
| "notify"
| "fs.read"
| "fs.write"
| "fs.delete"
| "agent.tool.register"
| "agent.prompt.inject"
| "provider.register"
| "net.fetch"
| "shell.openExternal"
| "mcp.server.local"
| "mcp.server.remote"
| "background.service"
| "bus.publish"
| "bus.subscribe"
| "browser.cdp"
| "desktop.control"
| "ui.microphone"
| "project.create"
| "session.import"
| "session.read.own"
| "session.update.own"
| "session.delete.own"
| "usage.read"
| "audio.capture.background"
| "audio.playback.background"
| "speech.adapter.register"
| "keyboard.globalShortcut"
| "net.websocket";未知权限=验证失败。
fs.read.workspace、fs.write.workspace、fs.delete.workspace 是文件范围机制 之前的旧权限名。它们仍然能通过校验,主机会在加载时把它们改写成最小安全等价物 (§5.2);新的清单不得再使用。
5.2 fs —— 文件权限可以触碰哪些路径
type PluginFsPolicy = {
read?: PluginFsRule;
write?: PluginFsRule;
delete?: PluginFsRule;
};
type PluginFsRule = {
root?: "workspace" | "userSelected"; // 默认 `workspace`
scope?: string[]; // 相对 root 的 glob
own?: boolean; // 仅删除:这个插件写过的路径
};权限回答的是「这个插件能不能碰文件」,这里回答的是「能碰哪些文件」。 glob 里 * 匹配一个路径段,** 跨分隔符匹配,并以大小写不敏感的方式 与相对 root 的路径比较。
{
"permissions": ["fs.read", "fs.write", "fs.delete"],
"fs": {
"read": { "root": "workspace", "scope": ["**/*"] },
"write": { "root": "workspace", "scope": ["docs/**", "*.md"] },
"delete": { "own": true, "scope": ["dist/**"] }
}
}- 没有
fs块、缺少某个模式、或scope为空都是合法的,含义是没有常驻 可达范围:每次访问都落到运行时确认 —— 什么都不说就什么都不授予 root: "userSelected"不需要 scope —— 用户通过pi.fs.requestDirectory()亲手选的目录本身就是授权,它只存在于内存中,并随插件进程一起消失own只在delete上被接受
5.3 net —— 出网白名单
type PluginNetDomains = string[]; // "api.example.com" 或 "*.example.com"主机掌握的每一条出网路径 —— 面板 session、pi.net.fetch、远程 HTTP MCP 端点 —— 都被限定在这些主机名之内。列表缺失、为空或非法就完全不放行出网, 无论 net.fetch 怎么声明。条目是裸主机名:没有 scheme、没有端口、没有路径, 也不允许裸 *。前缀 *. 同时覆盖该域名及其子域名。
pi.net.websocket 听同一份列表(net.websocket, 03-plugin-api.md §3)。该权限已实现: 连接被限定在 manifest.net.domains 之内,未被声明的主机会在传输被要求 打开任何东西之前就被拒绝。
5. 1 总线主题语法
主题最多是与 [a-zA-Z0-9][a-zA-Z0-9_-]* 匹配的点分隔段 8段128个字符。 contributes.bus.publish 列出了具体主题; contributes.bus.subscribe 列出 * 与一个段完全匹配的模式 ** 匹配一个或多个尾随段(仅最后一个段)。
{
"bus": {
"publish": ["build.done"],
"subscribe": ["build.*", "deploy.**"]
}
}5.4 providers —— 插件声明的 provider 行
contributes.providers 最多声明 8 个 provider,宿主会把每一项落成原生 provider 列表中的一行,并归该插件所有(ADR 0259):
- 声明的
id匹配[a-zA-Z][a-zA-Z0-9_-]{0,63}且在插件内唯一;行 id 为plugin:<pluginId>:<declaredId> name必填,是设置页显示的名称baseUrl可选,但必须是绝对http(s)URLapiStyle可选,默认chat_completions;可取值是 provider 配置中除auto以外的风格authKind可选,为api_key(默认)或nonemodels要求 1..64 条,id 唯一且长度为 1..256
非空的 contributes.providers 需要高风险权限 provider.register (13-plugin-permissions-matrix.md)。 声明会在每次插件加载时重新读取,并对其自身字段具有权威;禁用插件会保留这些行并 将其关闭,而删除声明或卸载插件会连同已存凭据一起删除该行。
oauth 暂不支持:宿主还没有插件 OAuth 登录流程,因此 oauth 块或 authKind: \"oauth\" 会在清单元数据校验阶段被拒绝。计划中的 provider.oauth 权限与宿主自有的登录流程属于未来工作,当前不可用。
6. activationEvents(可选)
示例:
onStartuponCommand:demo.hello.sayonAgentModeonWorkspaceOpen
MVP 只能实现:
onStartuponCommand:*
7. 验证规则
schemaVersion必须是1- 需要
id/name/version - 声明
ui.panel的清单是否需要隐式(自动填充)或通过显式声明获得ui.panel权限是一个 悬而未决的问题(在 08-meta/open-questions.md 中跟踪) - 如果存在
agentTools,则必须声明agent.tool.register - 路径字段不得使用绝对路径或
.. main/ui.panel/ 技能 /views[].entry路径必须存在 7.工具name仅允许[a-zA-Z][a-zA-Z0-9_]*- 贡献 ID(
themes、mcpServers、services、views)必须匹配[a-zA-Z][a-zA-Z0-9_-]{0,63}并在自己的列表中保持唯一;sessionSources允许额外使用. themes[].path必须存在且以.css结尾;themes[].base可能只是light或darkmcpServers[]必须准确设置一个传输字段:stdio要求command(裸路径名称或插件相对,从不绝对)并拒绝url/headers;http需要绝对的http或httpsurl,并拒绝command/args/env。非回环 HTTP 不加密,必须声明在net.domains中。bus.publish条目必须是具体主题,bus.subscribe条目必须是具体主题 有效模式(§5.1)- 需要权限的贡献在权限验证时失败 缺少:
themes→ui.theme,views→ui.view,providers→provider.register,stdio 服务器 →mcp.server.local,远程 服务器 →mcp.server.remote、services→background.service、bus.publish→bus.publish,bus.subscribe→bus.subscribe。skills是一个例外 - 它早于权限门,因此清单 没有agent.prompt.inject仍然有效并且运行时只是跳过 技能 - 设置的 key 必须唯一。
shortcut设置必须带command,只能用plugin作用域,并按「修饰键 + 按键」或 F 键校验。在安全的插件密钥存储出现之前, secret 一律拒绝 fs.<mode>需要对应的fs.<mode>权限 —— 没人能用的范围是作者的笔误, 不是静默的空操作。scope 条目必须是相对路径(不得是绝对路径、盘符或..), 并且fs.write/fs.delete不得使用整棵树的模式(**、**/*、*/**、./*)。own只在delete上被接受,root只能是workspace/userSelectednet.domains条目必须是裸主机名,可选前缀*.;裸*会被拒绝views[].title必填;使用本地化对象时必须同时提供en与zh-CN。views[].icon不按 token 列表校验:未知 token 会降级为字母瓷砖, 为一个纯外观细节拒绝插件并不合理。打包检查会改为给出警告sessionSourcesid 必须匹配[a-zA-Z][a-zA-Z0-9._-]{0,63}且不能重复; 本地化 label 必须同时提供en和zh-CNcontributes.globalShortcuts最多允许 8 条,且需要keyboard.globalShortcut。每个id匹配[a-zA-Z][a-zA-Z0-9._-]{0,63}且唯一;command必须声明在contributes.commands里;default若存在,使用与shortcut设置相同的 修饰键加按键 / F 键语法
8. 示例:最小插件
{
"schemaVersion": 1,
"id": "demo.hello",
"name": "Hello",
"version": "0.1.0",
"main": "main.js",
"ui": {
"panel": "renderer/index.html"
},
"contributes": {
"commands": [
{
"id": "hello.open",
"title": "Open Hello Panel",
"keywords": ["hello"]
}
]
},
"permissions": ["ui.panel"]
}9. 示例:Agent 工具插件
{
"schemaVersion": 1,
"id": "demo.echo-tool",
"name": "Echo Tool",
"version": "0.1.0",
"main": "main.js",
"contributes": {
"agentTools": [
{
"name": "echo_text",
"description": "Echo a text value",
"risk": "low",
"schema": {
"type": "object",
"properties": {
"text": { "type": "string" }
},
"required": ["text"]
}
}
]
},
"permissions": ["agent.tool.register"]
}9. 1 示例:能力贡献
{
"schemaVersion": 1,
"id": "demo.capabilities",
"name": "Capabilities",
"version": "0.1.0",
"main": "main.js",
"contributes": {
"skills": [{ "path": "skills/release.md", "id": "release-notes" }],
"themes": [
{ "id": "midnight", "label": "Midnight", "path": "themes/midnight.css", "base": "dark" }
],
"mcpServers": [
{
"id": "docs",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@example/docs-mcp"],
"env": { "DOCS_TOKEN": { "setting": "docsToken" } }
},
{
"id": "issues",
"transport": "http",
"url": "https://mcp.example.com/issues",
"headers": { "Authorization": { "setting": "issuesAuth" } }
}
],
"services": [{ "id": "watcher", "label": "Repo watcher" }],
"bus": { "publish": ["demo.build.done"], "subscribe": ["demo.**"] }
},
"permissions": [
"agent.prompt.inject",
"ui.theme",
"mcp.server.local",
"mcp.server.remote",
"background.service",
"bus.publish",
"bus.subscribe"
]
}{ "setting": "<key>" }读取插件自身的设置;宿主环境是 从未通过(D018)。
10. 兼容性策略
- 未来的
schemaVersion: 2需要迁移器 - 主机应拒绝过高的主要版本
- 未知的可选字段可能会被忽略;未知所需的权限必须失败