Skip to content

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

  • 状态:已接受(v1 为过渡方案,演进路径已明确)
  • 落地进度:尚未开始。插件系统是 M5,本文档的所有内容目前都是设计, 一行实现都没有。唯一已经兑现的是最后那条 —— PluginContext 上的方法一律 返回 Promise(见 @mosu/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 之后的明确演进项,并在此期间对用户如实说明限制。

具体:

  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 无关联,是一个独立实现。