ADR-0004 · 插件隔离与权限模型
- 状态:已接受(v1 为过渡方案,演进路径已明确)
- 落地进度:尚未开始。插件系统是 M5,本文档的所有内容目前都是设计, 一行实现都没有。唯一已经兑现的是最后那条 ——
PluginContext上的方法一律 返回 Promise(见@typo/plugin-api),那是为将来的进程隔离预留的。 - 日期:设计阶段
背景
插件生态是这类编辑器活下去的关键,但插件也是最大的安全风险来源: 用户从社区装一个「导出到 Notion」的插件,它拿到的是什么权限?
Electron 环境下,如果插件直接跑在渲染进程且能拿到 preload 暴露的 API, 那它实际上能做的事约等于「本机上以用户身份运行任意代码」。
可选方案
| 方案 | 隔离强度 | 开发体验 | 实现成本 |
|---|---|---|---|
| A · 渲染进程内直接加载 | 无 | 最好(同步调用 DOM、编辑器 API) | 低 |
| B · 渲染进程内加载 + 权限声明 + 敏感能力经 main 二次校验 | 弱(防误用不防恶意) | 好 | 中 |
| C · Worker / utility 进程 + RPC,DOM 操作通过声明式 API | 强 | 差(全异步、无法直接碰 DOM) | 高 |
| D · WASM / QuickJS 沙箱 | 最强 | 很差(生态几乎不可用) | 很高 |
决策
v1 采用 B;把 C 列为 1.0 之后的明确演进项,并在此期间对用户如实说明限制。
具体:
- 插件在 manifest 里声明所需权限(类型 + 人类可读的
reason); - 安装时向用户展示权限清单,用户确认后才加载;
- 文件系统、网络、执行外部命令这三类能力不在渲染进程直接执行: 插件调用的是一个 IPC 代理,请求到 main 进程后,main 依据 「该插件已授权的权限 + 路径/域名白名单」再校验一次才执行;
- 插件注册的一切(命令、面板、语法、事件监听)由
PluginContext记账, 卸载 / 禁用时自动注销,不依赖插件作者写对清理逻辑; - 插件异常不得影响主程序:所有插件回调包在错误边界里, 连续抛错的插件自动禁用并提示用户。
为什么 v1 不直接上 C
诚实的理由是:C 会让 v1 做不完,且会让插件 API 的形状在需求还没稳定时就被锁死。
方案 C 要求所有编辑器 API 都是异步且可序列化的。这意味着:
- 语法扩展(03 §2 的三件套)里的 Lezer 解析函数、装饰规则函数 都必须跨进程传递 —— 函数不能序列化,只能改成声明式 DSL;
- 而这个 DSL 要覆盖多少表达力,在还没有真实插件案例之前根本设计不出来。
正确的顺序是:先用 B 让生态跑起来,观察真实插件都在做什么, 再据此设计 C 的声明式边界。反过来做几乎必然设计错。
必须履行的诚实义务
采用 B 意味着我们无法在技术上阻止恶意插件。因此以下几条是决策的组成部分, 不是可选的沟通技巧:
- 插件市场 / 文档页面必须明确写出:「插件与编辑器运行在同一进程, 权限提示可以帮你识别插件想做什么,但无法阻止蓄意作恶的插件。请只安装可信来源的插件。」
- 不使用「沙箱」「安全隔离」这类会让用户误以为有强隔离的措辞。
- 首次安装第三方插件时给一次性的醒目提示。
- 官方仓库的插件应有人工审核 + 源码可查 + 构建可复现的要求。
演进到 C 的触发条件与路径
触发条件(满足任一即启动):
- 出现真实的恶意插件事件;
- 插件数量达到官方审核跟不上的规模(例如 > 200 个);
- 有企业用户提出隔离要求。
路径:
- 先把插件 API 中所有已经是异步的部分标记为「隔离安全」;
- 统计现有插件对同步 DOM / 编辑器 API 的实际用法,据此设计声明式替代;
- 提供双运行模式:新 API 的插件跑在隔离进程,旧插件继续跑在渲染进程并标注「未隔离」;
- 给一个 major 版本的迁移期,之后停止加载未隔离插件。
这条路径的前提是 API 从一开始就尽量异步。因此即便 v1 是同进程, PluginContext 上的方法也一律返回 Promise —— 现在多写几个 await, 换将来不用推倒重来。