02. Plugin Manifest Schema
1. Purpose
Freeze the plugin manifest fields to guarantee:
- The host can validate
- Developers can depend on it
- Future versions can migrate
Schema Version: 1
2. Root object
ts
type PluginManifestV1 = {
schemaVersion: 1;
id: string; // ^[a-z0-9]+(\.[a-z0-9_-]+)+$
name: string;
version: string; // semver
description?: 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[];
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
};3. UI config
ts
type PluginUiConfig = {
panel?: string; // html entry
width?: number;
height?: number;
resizable?: boolean;
title?: string;
};4. contributes
ts
type PluginContributes = {
commands?: PluginCommandContrib[];
agentTools?: PluginAgentToolContrib[];
skills?: Array<string | PluginSkillContrib>; // relative paths, or metadata overrides
settings?: PluginSettingContrib[];
themes?: PluginThemeContrib[];
mcpServers?: PluginMcpServerContrib[];
services?: PluginServiceContrib[];
bus?: PluginBusContrib;
};
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";
default?: unknown;
enum?: Array<{ label: string; value: string | number | boolean }>;
secret?: boolean;
};
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`
};
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 only
url?: string; // https, or http when the host is loopback
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.**`
};5. permissions enum
ts
type PluginPermission =
| "ui.panel"
| "ui.theme"
| "clipboard.read"
| "clipboard.write"
| "notify"
| "fs.read.workspace"
| "fs.write.workspace"
| "fs.delete.workspace"
| "agent.tool.register"
| "agent.prompt.inject"
| "net.fetch"
| "shell.openExternal"
| "mcp.server.local"
| "mcp.server.remote"
| "background.service"
| "bus.publish"
| "bus.subscribe";Unknown permission = validation failure.
5.1 Bus topic grammar
Topics are dot-separated segments matching [a-zA-Z0-9][a-zA-Z0-9_-]*, at most 8 segments and 128 characters. contributes.bus.publish lists concrete topics; contributes.bus.subscribe lists patterns where * matches exactly one segment and ** matches one or more trailing segments (final segment only).
json
{
"bus": {
"publish": ["build.done"],
"subscribe": ["build.*", "deploy.**"]
}
}6. activationEvents (optional)
Examples:
onStartuponCommand:demo.hello.sayonAgentModeonWorkspaceOpen
MVP may implement only:
onStartuponCommand:*
7. Validation rules
schemaVersionmust be1id/name/versionare required- Whether a manifest that declares
ui.panelneeds theui.panelpermission implicitly (auto-filled) or by explicit declaration is an open question (tracked in 08-meta/open-questions.md) - If
agentToolsare present,agent.tool.registermust be declared - Path fields must not use absolute paths or
.. main/ui.panel/ skills paths must exist- tool
nameallows only[a-zA-Z][a-zA-Z0-9_]* - Contribution ids (
themes,mcpServers,services) must match[a-zA-Z][a-zA-Z0-9_-]{0,63}and be unique within their own list themes[].pathmust exist and end in.css;themes[].basemay only belightordarkmcpServers[]must set exactly one transport's fields:stdiorequirescommand(bare PATH name or plugin-relative, never absolute) and rejectsurl/headers;httprequiresurl(https, orhttponly for loopback) and rejectscommand/args/envbus.publishentries must be concrete topics andbus.subscribeentries valid patterns (§5.1)- A contribution that needs a permission fails validation when the permission is missing:
themes→ui.theme, stdio servers →mcp.server.local, remote servers →mcp.server.remote,services→background.service,bus.publish→bus.publish,bus.subscribe→bus.subscribe.skillsis the exception — it predates the permission gate, so a manifest withoutagent.prompt.injectstill validates and the runtime simply skips the skills
8. Example: minimal plugin
json
{
"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. Example: Agent Tool plugin
json
{
"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 Example: capability contributions
json
{
"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>" } reads the plugin's own settings; the host environment is never passed through (D018).
10. Compatibility strategy
- A future
schemaVersion: 2needs a migrator - The host should reject a too-high major version
- Unknown optional fields may be ignored; unknown required permissions must fail