周毓杰

Plugin 开发手册

本文面向 Open Nua Plugin 作者,说明如何把一个领域能力交付为可安装、可审计、可治理的 Plugin。当前契约以 Agent Plugins v1 作为可移植核心,以 com.opennua.desktop 扩展承载 Open Nua 专有能力。

Plugin 包是可版本化的交付单元:Hub 校验包和 manifest,Gateway 只代理已批准的工具面, Desktop 只加载已安装且启用的 Adapter、Skill 和受控 App。Plugin 不通过 manifest 自行 指定任意 upstream URL,也不获得未声明的网络、文件或账户权限。

发布参数和 Hub 认证见Hub 发布手册。官方包可参考 workspace/plugins/packages

1. Plugin 与 Skill 的边界

  • Plugin 拥有可执行 Adapter、Capability 声明、App/Agent 贡献、版本、安装和启用状态。
  • Skill 是给 Agent 阅读的操作知识,说明适用场景、工具选择、步骤和边界。
  • Plugin 可以贡献一个或多个标准 Skill,但 Skill 本身不获得执行权限。

安装成功后 Plugin 默认停用;只有 installed + enabled 的 Plugin 才能向新一轮会话贡献 Skill、Agent context 或 Capability。禁用不删除本地状态,卸载也不会自动清除状态文档。

如果场景只需要提示词和已有工具,优先发布 Skill;需要新增受控能力、领域 App 或持久化 本地状态时,才开发 Plugin。

2. 选择 Adapter

1.2.0 的 Open Nua Plugin 有两类 Adapter,另允许没有 Adapter 的纯 Skill Plugin:

Adapter适用场景能力提供方状态与部署
gateway_mcp集中部署、工具调用、需要统一数据和服务治理Gateway 受控 MCP 代理领域服务由组织部署,Desktop 不携带服务依赖
local_state离线、本地优先的领域 AppDesktop Host 的本地状态和只读领域上下文每个用户与 Plugin 隔离,单一 JSON 文档,最大 64 KiB

gateway_mcplocal_state 在 1.2.0 中不能同时声明。App、声明式 UI、Agent Definition 和标准 Skill 是独立贡献,可以与任一 Adapter 组合。

远程服务仍由部署管理员配置。manifest 只声明 Gateway 路径、工具白名单和 timeout;客户端 不能提交或覆盖 upstream URL。生产环境由 Backend 的已审核 runtime policy 连接私网服务, 未发布、未启用或未映射的服务必须返回 unavailable。

3. 包目录

每个归档必须只有一个顶层目录,并至少包含 PLUGIN.md、根 plugin.json 和一个标准 Skill:

my-plugin/
├── PLUGIN.md
├── USER_GUIDE.zh-CN.md              # 推荐
├── plugin.json
├── skills/
│   └── my-plugin/
│       └── SKILL.md
└── com.opennua.desktop/             # 仅当使用 Open Nua 扩展时
    ├── agents/
    │   ├── my-plugin.json
    │   └── my-plugin.md
    └── ui/
        └── my-plugin.html

Open Nua 专有文件只能位于 com.opennua.desktop/。不要把 .env.git.venv__pycache__node_modules 或运行时生成的数据放入归档;归档不能包含符号链接、硬链接、 特殊文件、路径穿越或多个顶层目录。

4. 根 plugin.json

根 manifest 使用 Agent Plugins v1 核心字段。核心身份字段不包含 Open Nua runtime:

{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
  "name": "my-market-research",
  "version": "1.0.0",
  "description": "Research public market data without trading.",
  "license": "UNLICENSED",
  "extensions": {
    "com.opennua.desktop": {
      "schema_version": 2,
      "runtime": {
        "gateway_mcp": {
          "name": "market-research",
          "gateway_path": "/api/plugins/market-research/mcp",
          "tools": ["get_market_data"],
          "timeout_seconds": 60,
          "tool_name_prefix": true
        }
      }
    }
  }
}

name 使用小写字母、数字、连字符和点,长度为 1–64;不能以分隔符结尾,也不能包含 连续的连字符或点。version 使用 SemVer。未知的 extension namespace 可以被通用客户端 忽略,Open Nua 字段不得泄漏到核心身份字段。

4.1 gateway_mcp

{
  "runtime": {
    "gateway_mcp": {
      "name": "market-research",
      "gateway_path": "/api/plugins/market-research/mcp",
      "tools": ["get_market_data", "get_company_profile"],
      "timeout_seconds": 60,
      "tool_name_prefix": true
    }
  }
}

工具白名单数量为 1–64,工具名只使用字母、数字、下划线和连字符,长度不超过 128; timeout_seconds 为 1–300。可选的 model_delegation.tools 只能引用同一白名单中的 工具,不会授予 Plugin 长期模型凭据。

4.2 local_state

{
  "runtime": {
    "local_state": {
      "state_schema_version": 1,
      "state_max_bytes": 65536
    }
  }
}

Host 以用户和 Plugin 为边界存储一个 schema-versioned JSON 文档,使用临时文件加原子替换 写入。state_schema_version 只描述当前 Plugin 的状态契约;Host 不执行任意 Plugin 代码,也不 会静默迁移或清空旧文档。版本不匹配时返回 LOCAL_STATE_MIGRATION_REQUIRED,并保留旧 state、 revision 与 expected version;Plugin App 必须显式完成迁移后再调用 setLocalState。损坏文档 返回 LOCAL_STATE_RECOVERY_REQUIRED,由 Plugin 提供用户确认的恢复路径。 本地 App 不能直接访问文件系统、浏览器存储、Node API 或网络。

4.3 Capability

contributes.capabilities 必须显式声明 provider、audience、operation、risk、确认和幂等策略:

{
  "id": "my-market-research.get_market_data",
  "provider": "gateway_mcp",
  "target": "get_market_data",
  "audience": "both",
  "operation": "query",
  "risk": "low",
  "requiresConfirmation": false,
  "requiresHitl": false,
  "idempotency": "none"
}

gateway_mcp capability 的 target 必须出现在 Adapter 工具白名单中。desktop_local_context 只能用于本地只读 query projection,并且必须声明 contextType。App 使用的 capability 必须对 appboth audience 开放;Agent、App 和声明式 UI 共用同一套 Capability 校验。

4.4 Agent Definition、声明式 UI 和 App

Agent Definition 位于 com.opennua.desktop/agents/,manifest 只索引相对路径:

{
  "agents": [
    {
      "agentId": "my-market-research-assistant",
      "default": true,
      "definition": "com.opennua.desktop/agents/my-market-research-assistant.json"
    }
  ]
}

contributes.ui 是受控声明式 Host UI;contributes.app 是经过 SHA-256 校验的领域 App:

{
  "app": {
    "protocolVersion": "1",
    "resourceUri": "ui://my-market-research/market-research.html",
    "resourcePath": "com.opennua.desktop/ui/market-research.html",
    "mimeType": "text/html;profile=mcp-app",
    "sha256": "<64 lowercase hex characters>",
    "capabilities": ["my-market-research.get_market_data"]
  }
}

resourceUri 必须属于当前 Plugin,resourcePath 必须位于 com.opennua.desktop/ui/。App 只能通过 Host Bridge 调用已声明且已授权的 Capability, 不能直接调用 IPC、模型、MCP、网络或文件系统。

5. 标准 Skill 与可选 MCP

标准 Skill 固定放在 skills/<name>/SKILL.md,frontmatter 至少包含非空的 namedescription

---
name: My Market Research
description: Research public market data without trading or account access.
---

# My Market Research

Use only the declared read-only capabilities and state the source and limitations.

只有确实需要向通用 Agent Plugins 客户端暴露标准 MCP endpoint 时,才提供根 mcp.json

{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
  "mcpServers": {
    "my-market-research": {
      "command": "my-market-research-mcp",
      "args": ["serve"]
    }
  }
}

没有根 mcp.json 的包仍然是有效的标准 Skill Plugin,但普通客户端不会获得 Open Nua 的 Gateway、App 或 local-state 能力。三个当前官方包明确属于 Open Nua extension-only MCP: Vibe Trading 和 DeepTutor 使用受控 Gateway Adapter,抓娃娃使用 local-state Adapter,均不 提供根 mcp.json

6. 本地开发与 plugins:dev

官方 Plugin 的 runtime 和 App 源码由 plugins 仓维护。workspace 根运行:

npm run plugins:dev

该命令从每个 packages/*/plugin.json 生成短租约 development registry,启动 loopback-only runtime 和 Vite App server;它不读取旧的隐藏 manifest 路径。默认 runtime、upstream 和 UI 端口分别由脚本输出,也可通过脚本支持的环境变量覆盖。开发 registry 只供本地 Gateway 和 Electron 使用,退出命令后会失效;packaged Desktop 和 prod-sim 不读取它。

官方 App 源码保存于各 Plugin 自己的目录,构建产物写入 packages/<plugin>/com.opennua.desktop/ui/。构建和测试:

npm run plugins:ui:typecheck
npm run plugins:ui:build
npm run plugins:test

本地 Desktop 中选择“资产库 → 插件 → 加载本地插件”,选择包含 PLUGIN.mdplugin.jsonskills/ 的源码目录。Desktop 会创建指向源码的本地链接;校验失败不会替换原安装。

源码修改后可以用 CDP reload:

npm run plugin -- reload-local my-market-research
npm run plugin -- reload-local plugins/packages/my-market-research \
  --cdp-url http://127.0.0.1:9225 --json

reload 会重新校验根 manifest、Skill、Agent、App digest 和 Adapter,并保留启用状态。修改 Capability、工具集合或 Skill 后必须使用新会话验收;旧会话保留的是当时的能力快照。

7. 测试与验收

包级测试至少覆盖:

  • PLUGIN.md、所有 SKILL.md frontmatter 和根 plugin.json 可解析;
  • Agent Definition、App resource path 和每个 asset digest 均存在且一致;
  • 可选 mcp.json 的 schema、mcpServers 和 transport 声明合法;
  • 归档没有链接、路径穿越、环境文件或生成数据;
  • Plugin 未启用时 Skill、Capability 和 App 均不可用;
  • Gateway target 白名单、Capability provider/audience/operation/context 一致;
  • local-state 的用户隔离、64 KiB 上限、迁移、原子写和损坏恢复;
  • 最终用户任务、会话复用、错误提示和恢复路径形成闭环。

workspace 根的官方包与 Hub 兼容性检查:

npm run plugins:test
UV_CACHE_DIR=/tmp/openneo-uv-cache uv run --directory services/backend pytest -q tests/test_plugin_archive.py

Desktop 还应运行 npm run typecheck、focused Node/Renderer tests、uv run ruff check src/agent_bridge,并用 Electron CDP 验收远程 App、离线 local-state App、能力 unavailable、 损坏状态和会话恢复。不要只验证进程没有报错,要检查用户可见结果。

8. 打包与发布

归档示例:

COPYFILE_DISABLE=1 tar -czf /tmp/my-plugin-1.0.0.tgz \
  --exclude='.DS_Store' --exclude='.env' --exclude='.git' \
  --exclude='.venv' --exclude='__pycache__' --exclude='node_modules' \
  -C plugins/packages my-plugin

tar -tzf /tmp/my-plugin-1.0.0.tgz

首次发布和后续版本使用 Hub 发布器:

npm run publish:plugin -- \
  --archive /tmp/my-plugin-1.0.0.tgz \
  --slug my-plugin --version 1.0.0 \
  --visibility authenticated --tags research

npm run publish:plugin -- \
  --archive /tmp/my-plugin-1.1.0.tgz \
  --asset-id <plugin-uuid> --version 1.1.0

Plugin 版本不可覆盖。Backend 是 package、manifest、Skill、App digest、MCP 和版本合法性 的最终校验方;CLI 不绕过服务端授权。

9. 常见问题

Plugin 列表有包但 Workbench 无法打开

先确认 Plugin 为 installed + enabled,再检查 App 的 resourcePath 是否位于 com.opennua.desktop/ui/、文件和 SHA-256 是否匹配。若是本地 Plugin,执行 reload;修改 源码后重启 Desktop Main 进程,避免继续使用旧的 Main bundle。

远程 Adapter unavailable

manifest 不会启动远程服务。检查已发布版本、Gateway policy、gateway_path、scope、工具 白名单和受控 upstream 是否一致。不要在 Plugin 中加入专用 URL 配置。

会话列表无法加载

Workbench 会话仍然复用 Desktop 的标准 Thread;Plugin 不维护私有聊天存储。检查当前 Plugin 和 context 是否已启用、Capability 是否对 App/Agent 开放,以及 Gateway 或 local context 返回的错误是否被 Host 显示为可恢复状态。切换领域对象不会静默改绑旧会话。

10. 发布前清单

  • plugin.json 使用 Agent Plugins v1 schema,Open Nua 字段全部在扩展 namespace。
  • 所有 Skill 位于 skills/<name>/SKILL.md,frontmatter 可解析。
  • 如提供标准 MCP,根 mcp.json schema 和 transport 校验通过;否则记录 extension-only 边界。
  • Adapter、Capability provider/audience/operation/context 和工具白名单一致。
  • App、Agent Definition、asset path 和 digest 通过归档校验。
  • local-state 通过隔离、上限、迁移、原子写和损坏恢复测试。
  • plugins:devplugins:test、Desktop focused tests、typecheck、ruff 和 Backend pytest 通过。
  • 完成远程 App、local-state App、unavailable、损坏状态和会话恢复的 Electron CDP 走查。