Skip to content

ADR-0004 · 插件隔离与权限模型 ​

  • 状态:已搁置(2026-08)—— 被 ADR-0006 收窄
  • 落地进度:尚未开始,且不会在 v1 开始。插件系统原本是 M5,ADR-0006 把 M5 整个换成了 AI 基座。本文档的所有内容都是设计,一行实现都没有。
  • 日期:设计阶段

为什么不删这份 ADR。

ADR-0006 决定 v1 不做开放插件系统,但它没有推翻下面这份分析 —— 它推翻的是 「现在就要做」这个前提。四种隔离方案的比较、B 为什么在生态起步期优于 C、 以及「先让生态跑起来再据此设计声明式边界」的推理,插件回来的那天原样有效。

唯一要在读的时候替换掉的是它的起点:ADR-0006 D6 定的开放顺序是 Tool API(受控开放)→ 观察真实用法 → 再谈 Plugin SDK, 所以「观察真实插件都在做什么」这一步,将来观察的对象是 Tool API 的用法。

重估条件写在 ADR-0006 的「什么时候重新考虑插件」。

另外一条要更正:下面写着「PluginContext 上的方法一律返回 Promise 是唯一已经 兑现的部分」—— 那句话当时就不准确,@mosu/plugin-api 里只有 HostBridge, 从来没有过 PluginContext。包名的事见 01 §2。

背景 ​

插件生态是这类编辑器活下去的关键,但插件也是最大的安全风险来源: 用户从社区装一个「导出到 Notion」的插件,它拿到的是什么权限?

Electron 环境下,如果插件直接跑在渲染进程且能拿到 preload 暴露的 API, 那它实际上能做的事约等于「本机上以用户身份运行任意代码」。

可选方案 ​

方案隔离强度开发体验实现成本
A · 渲染进程内直接加载无最好(同步调用 DOM、编辑器 API)低
B · 渲染进程内加载 + 权限声明 + 敏感能力经 main 二次校验弱(防误用不防恶意)好中
C · Worker / utility 进程 + RPC,DOM 操作通过声明式 API强差(全异步、无法直接碰 DOM)高
D · WASM / QuickJS 沙箱最强很差(生态几乎不可用)很高

决策 ​

v1 采用 B;把 C 列为 1.0 之后的明确演进项,并在此期间对用户如实说明限制。

具体:

  1. 插件在 manifest 里声明所需权限(类型 + 人类可读的 reason);
  2. 安装时向用户展示权限清单,用户确认后才加载;
  3. 文件系统、网络、执行外部命令这三类能力不在渲染进程直接执行: 插件调用的是一个 IPC 代理,请求到 main 进程后,main 依据 「该插件已授权的权限 + 路径/域名白名单」再校验一次才执行;
  4. 插件注册的一切(命令、面板、语法、事件监听)由 PluginContext 记账, 卸载 / 禁用时自动注销,不依赖插件作者写对清理逻辑;
  5. 插件异常不得影响主程序:所有插件回调包在错误边界里, 连续抛错的插件自动禁用并提示用户。

为什么 v1 不直接上 C ​

诚实的理由是:C 会让 v1 做不完,且会让插件 API 的形状在需求还没稳定时就被锁死。

方案 C 要求所有编辑器 API 都是异步且可序列化的。这意味着:

  • 语法扩展(03 §2 的三件套)里的 Lezer 解析函数、装饰规则函数 都必须跨进程传递 —— 函数不能序列化,只能改成声明式 DSL;
  • 而这个 DSL 要覆盖多少表达力,在还没有真实插件案例之前根本设计不出来。

正确的顺序是:先用 B 让生态跑起来,观察真实插件都在做什么, 再据此设计 C 的声明式边界。反过来做几乎必然设计错。

必须履行的诚实义务 ​

采用 B 意味着我们无法在技术上阻止恶意插件。因此以下几条是决策的组成部分, 不是可选的沟通技巧:

  • 插件市场 / 文档页面必须明确写出:「插件与编辑器运行在同一进程, 权限提示可以帮你识别插件想做什么,但无法阻止蓄意作恶的插件。请只安装可信来源的插件。」
  • 不使用「沙箱」「安全隔离」这类会让用户误以为有强隔离的措辞。
  • 首次安装第三方插件时给一次性的醒目提示。
  • 官方仓库的插件应有人工审核 + 源码可查 + 构建可复现的要求。

演进到 C 的触发条件与路径 ​

触发条件(满足任一即启动):

  • 出现真实的恶意插件事件;
  • 插件数量达到官方审核跟不上的规模(例如 > 200 个);
  • 有企业用户提出隔离要求。

路径:

  1. 先把插件 API 中所有已经是异步的部分标记为「隔离安全」;
  2. 统计现有插件对同步 DOM / 编辑器 API 的实际用法,据此设计声明式替代;
  3. 提供双运行模式:新 API 的插件跑在隔离进程,旧插件继续跑在渲染进程并标注「未隔离」;
  4. 给一个 major 版本的迁移期,之后停止加载未隔离插件。

这条路径的前提是 API 从一开始就尽量异步。因此即便 v1 是同进程, PluginContext 上的方法也一律返回 Promise —— 现在多写几个 await, 换将来不用推倒重来。

MIT 许可发布。与 Typora 无关联,是一个独立实现。