编写一个 Plexus 扩展
你正在为本地 Plexus 实例编写扩展。扩展是一个运行时注册的 connector:一份 manifest,声明一个 source 和它贡献的 capability 条目。安装扩展只是让这些 capability 可被发现——不授予任何访问权。每次安装仍由人批准,每次授权仍由人签发。
这是你(编写扩展的 agent)要遵循的精简契约。完整规范见扩展规范。
1. 清单结构

{
"manifest": "plexus-extension/0.1",
"source": "my-tool", // SourceId; seeds every entry id (<source>.<name>)
"label": "My tool",
"transport": "local-rest", // default transport for caps that don't override
"capabilities": [ /* ExtensionCapabilityDecl[] */ ],
"secrets": [ /* ExtensionSecretRef[] (optional) */ ],
"serviceHint": { /* how to locate a local service (optional) */ }
}每个 ExtensionCapabilityDecl:
{
"name": "vault.write", // <noun>.<verb>; full id = <source>.<name>
"kind": "capability", // capability | skill | workflow
"label": "Write a vault note",
"describe": "Write/overwrite a note at {path}. Use when the user asks to save…",
"io": { "input": { "type": "object", "properties": { "path": {"type":"string"} } } },
"grants": ["write"], // verbs this cap requires: read | write | execute
"transport": "local-rest", // cli | local-rest | skill | workflow | stdio | ipc (no mcp)
"route": { /* transport routing — see §3 */ }
}好的 describe 是 agent 的相关性信号:说清它做什么、何时用、需要哪些输入。写具体——describe 含糊,agent 就发现不了这项 capability。
id 是 <source>.<name>——不要在 name 里重复 source
完整的 capability id 由网关自动加 source 前缀得来。source 为 user-profile 时,name: "read" 产出 id user-profile.read;而 name: "user-profile.read" 会产出重复的 user-profile.user-profile.read——它照样通过校验,所以这个错误是静默的。name 只写不含前缀的部分:source 下有多个名词时用 <noun>.<verb>(vault.read、vault.write),单一用途的 source 直接用 <verb>(read)。
2. EntryKind(条目种类)
- capability:通过传输方式实现的可调用项(
cli/local-rest/ipc/stdio)。 - skill —— 纯 markdown 使用指引,无 transport。
body: { format:"markdown", markdown }。 - workflow —— 通过
members[]组合已有条目(每个成员在注册后都必须可解析)。
3. 各传输方式对 route 的要求
route 只由拥有它的 transport 读取,核心从不读。按 transport 分:
cli(第二大 RCE 风险面)
"route": {
"bin": "ls", // bare binary name — NO path, NO shell metacharacters
"args": ["{dir}"], // argv template; {placeholders} fill from io.input
"allowedBins": ["ls"] // user-confirmed allow-list (part of the approval surface)
}local-rest(第三大 SSRF / secret 重定向风险面)
"route": {
"baseUrl": "http://127.0.0.1:27123", // loopback by default; a non-loopback host is opt-in and
// requires an explicit, user-confirmed `allowedHosts` entry
// (the approval surface) — see `transport-policy.ts`
"allowedHosts": ["127.0.0.1:27123"], // host allow-list (part of the approval surface)
"method": "PUT",
"pathTemplate": "/vault/{path}", // canonical URL path key (`path` is a legacy alias)
"secret": { "name": "vault-key", "attach": "bearer" } // references secrets[] by name
}secret 的值从不出现在 manifest 里——值存放在 ~/.plexus/secrets/<name>,由 transport 在派发时附上。
skill / workflow
- skill:无
route;提供body。 - workflow:不设
route;提供members[],填写已存在条目的 ID。跨来源挂载(skill/workflow 引用其他来源的条目)默认关闭,因为它是提示注入通道;只有明确放行并经人工确认后才能启用。
4. 需要人工批准的安全事项
安装时,用户会看到具体的审批内容:扩展可以启动的 cli 二进制程序、可以访问的非回环 rest 主机、所有跨来源的 skill 挂载、每个 capability 所需的动词,以及扩展是否通过传输方式实现。申请的资源和权限应尽量少,只申请实际需要的二进制程序、主机和动词。
5. 安装流程
- 获取本指南:
GET /admin/api/extensions/authoring-guide。 - 把 manifest 起草成 JSON。
- 预览(不提交):
POST /admin/api/extensions/preview,带{ manifest }。读valid/reasons[];valid:false就修正 manifest 后重新预览。把返回的surface(cli 二进制 / rest 主机 / 跨源 / 动词)展示给人。 - 安装(由人批准):
POST /admin/api/extensions,带{ manifest }。本地用户就是 connection-key 持有者,也就是批准人,因此这一步直接提交,并留下source.install审计。响应:{ ok, source, registered, revision, reason? }。 - 移除:
DELETE /admin/api/extensions/:source。
已安装的扩展在网关重启后仍在
通过管理接口安装的扩展会持久化到 ~/.plexus/extensions.json,并在启动时重放。网关重启后,扩展的能力会恢复,无需重新安装。DELETE 或移除操作也会将扩展从这份持久化存储中删除。
CLI 等价命令:plexus extension preview|add|list|remove。
6. 完整示例 —— local-rest 的 "vault write" 扩展
{
"manifest": "plexus-extension/0.1",
"source": "my-vault",
"label": "My local vault",
"transport": "local-rest",
"secrets": [{ "name": "my-vault-key", "attach": "bearer" }],
"capabilities": [
{
"name": "notes.read",
"kind": "capability",
"label": "Read a note",
"describe": "Read the markdown of a note at {path}. Use to fetch existing note content.",
"io": { "input": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } },
"grants": ["read"],
"transport": "local-rest",
"route": {
"baseUrl": "http://127.0.0.1:27123",
"allowedHosts": ["127.0.0.1:27123"],
"method": "GET",
"pathTemplate": "/vault/{path}",
"secret": { "name": "my-vault-key", "attach": "bearer" }
}
},
{
"name": "notes.write",
"kind": "capability",
"label": "Write a note",
"describe": "Create or overwrite the note at {path} with {content}. Use when saving content the user dictated.",
"io": { "input": { "type": "object", "properties": { "path": { "type": "string" }, "content": { "type": "string" } }, "required": ["path", "content"] } },
"grants": ["write"],
"transport": "local-rest",
"route": {
"baseUrl": "http://127.0.0.1:27123",
"allowedHosts": ["127.0.0.1:27123"],
"method": "PUT",
"pathTemplate": "/vault/{path}",
"body": "{content}",
"secret": { "name": "my-vault-key", "attach": "bearer" }
}
},
{
"name": "notes.howto",
"kind": "skill",
"label": "How to use my-vault",
"describe": "Usage guidance for my-vault.notes.read / notes.write.",
"grants": [],
"transport": "skill",
"body": { "format": "markdown", "markdown": "# my-vault\nRead with `notes.read { path }`; write with `notes.write { path, content }`. Paths are relative to the vault root." }
}
]
}这个扩展通过传输方式实现(local-rest),并且具备写入能力,因此审批内容会列出主机 restHosts: ["127.0.0.1:27123"],以及 my-vault.notes.write 所需的 write 动词。用户批准的正是这些资源和权限。
7. 最佳实践与自检
manifest 通过校验,还不等于扩展可信、好用。以下做法能让审批它的人信得过它,也让发现它的 agent 用得上它。
7a. 实现健康检查
source SHOULD 实现健康检查协议,让管理面板和发现它的 agent 都能看到其能力当前是否可用:
health(): Promise<{ status: "ok" | "degraded" | "unavailable" | "unknown", detail?: string }>ok——可访问且正常提供服务。degraded——服务仍在运行,但运行状态不正常。unavailable——已停止运行或无法访问。- 健康检查是可选的:允许不做任何操作,只报告
unknown。实现健康检查后,智能体就能避开不可用的源,避免在不知情的情况下发起调用而失败。 - 若未实现
health(),状态从checkRequirements()派生(如二进制缺失、主机不可达);若那里也没有信息,则退回"unknown"。
健康状态要与 source_unavailable invoke 错误(§7b)对得上:报告 unavailable 的 source,其 invoke 也应以 source_unavailable 失败,发现与派发才一致。
7b. 返回精确、语义化的错误
能力调用失败时,应向调用它的 agent 返回标准 Plexus 错误码,并附上清楚、易读的 message/detail,不要只返回没有具体说明的 500 或含糊的字符串。准确的错误信息能帮助 agent 重试、改用其他 source,或向用户说明具体出了什么问题。
用标准错误码:source_unavailable、transport_error、schema_validation_failed、grant_required(其余见规范)。
// BAD — opaque, unactionable:
{ "error": "failed" }
// GOOD — semantic code + a message the agent (or user) can act on:
{ "code": "source_unavailable",
"message": "Obsidian REST API not reachable at 127.0.0.1:27124 — is the plugin running?" }7c. 自检清单(安装前逐项检查)
调用 POST /admin/api/extensions 前,请完成以下各项检查,确认通过后逐项勾选:
- [ ] 清单验证通过——运行
plexus extension preview <manifest.json>,确认结果为valid:true,并核对预览输出中已声明的 cli 可执行文件和 rest 主机。 - [ ] 传输可达,访问限于允许的主机——默认允许访问回环地址(
127.0.0.1/localhost);非回环主机须显式启用,对应的allowedHosts条目属于需要用户批准的范围,必须经用户明确确认。参见transport-policy.ts。确认本地服务已启动且可达。 - [ ] secret 只按名引用 —— manifest 任何位置都不出现 secret 值。
- [ ] 能力描述准确——每项能力都有具体的
describe(做什么、何时使用、需要哪些输入)和准确的io结构定义;不夸大能力的作用。 - [ ] 健康检查已实现(或有意跳过)—— 是否实现
health()由你决定;跳过没问题,但要是刻意的选择,而非疏忽(§7a)。 - [ ] 错误语义化 —— 失败返回标准错误码 + 可读消息,而非 500 或
{error:"failed"}(§7b)。
8. 合规清单
- [ ]
manifest为"plexus-extension/0.1";source是未被保留的 id。 - [ ] 每个 capability 都有
name(<noun>.<verb>)、kind、label、具体的describe、grants、transport。 - [ ] cli capability:
bin(仅二进制名)+args+allowedBins。local-rest capability:回环baseUrl+allowedHosts+ secret 引用。 - [ ] secret 只按名引用(manifest 里无值)。
- [ ] 工作流引用的都是已有成员的 id;只有明确打算跨 source 附加时,才这样做。
- [ ] 安装前已成功预览(
valid:true);cli-bins / rest-hosts / verbs 的审批范围限于所需的最小范围。