跳到主要内容
TABTAB

导航

架构决策

插件平台中那些「看似可以更简单」的写法,及其未予采用的理由。每一条均附上被替代方案的问题所在。

子进程不是特权层

需要真正计算或原生 API 的插件可以声明 runtime.kind = "process", 由宿主拉起一个自带的子进程。但这个子进程回调宿主时同样要过这个插件的权限判定。

直觉上「进程级插件」听起来就该比 webview 插件更强,于是很自然会想给它开后门。 一旦开了,「webview 沙箱」这条不变式就只在沙箱里成立 —— 用户看到的声明为「该插件仅申请 screen:capture」,实际却可访问整个文件系统。

验签钉在消费点

索引拉取成功时验一次签名、安装时再验一次 —— 看起来是重复劳动。 实际上缓存对象与「它是可信的」这两件事之间没有绑定:缓存可能被未来的 某个代码路径写入(导入、调试、手动构造),而 sha256 只证明 包与索引一致,两者同时被替换时它照样通过。

把验签钉在「我要据此下载并安装东西」这个决定上,才谈得上信任锚。

卸载必须先杀进程

卸载流程里收子进程必须早于删目录。进程的工作目录就在插件目录里; 目录删了之后进程还在跑(Unix 上是能跑的,只是 cwd 变成一个已删除的 inode), 而用户点击「卸载」时的预期是「该插件被彻底移除」。

读本地文件:闸门在宿主,字节不走桥

插件有了 fs:read 就能搜索本机文件并读取其中一条。看起来最省事 的做法是让插件直接传路径、宿主直接读字节;实际做下来有两处不能省。

第一处是协议:本来可以把任意路径塞进已有的tabtab-plugin://。但那条通道是给插件自己目录内的文件用的, 一旦也服务用户磁盘,路径校验就退化成「协议层能不能猜到路径」—— 而它还要同时防 ../、符号链接与混合分隔符,多一个用途就多一处判据。 所以另开一条:fs.preview 换一个 token,宿主把字节暂存在内存里, 回一个 tabtab-file://<token>。协议层只按 token 查表,/、..、查询串在取值阶段就被丢掉了 ——协议层没有任何路径语义可攻。

第二处是路径本身。读路径闸门只接受绝对路径、已存在的普通文件、 不跟符号链接,并拒绝以 - 开头的输入(那是给外部命令当参数传时的注入)。

刻意不提供「把 ~/… 展开到任意根目录」的能力: 一旦允许前缀展开,就要引入一张可配置的根目录白名单,而那张表本身 就是新的越权面。绝对路径已经够用 —— 插件搜出来的每一个路径本来都是 绝对路径,用户点开预览的那一个也是。

重扫失败不能吞

安装完成后要重建插件注册表。早先这段写成 「扫描成功才继续」的形式,重扫失败就整段跳过, 然后照样广播事件、返回「安装成功」。

结果是插件文件已经在磁盘上,却不在内存注册表里 —— 面板里搜不到、唤不起, 而用户看到的提示是「安装成功」。这类「报告成功但实际没生效」比直接报错难查得多, 所以错误必须冒到用户眼前。

锁中毒 ≠ 值为空

所有取锁的地方走同一个入口。读路径宽松(拿不到锁就按「没有数据」处理并留痕), 写路径严格(拿不到锁就报错)。

这不是洁癖:静默地把「取不到锁」当成「集合为空」,会让一次重扫造出一份 「全部启用」的注册表,用户停用的插件在那条路径上被静默复活。

几何常量只在宿主定义一次

面板高度、窗口位置这类数值在前端写一遍、在 Rust 里再写一遍, 改了 Rust 侧而前端没跟上,两边就会漂移 —— 而且漂移的表现是「窗口位置有点怪」, 极难定位。

已知缺口

  • 公开市场只接受官方包。第三方包目前只能本地导入。
  • 子进程插件的分发链路已通(同意门 + 警示),但没有公开渠道。
  • 索引不支持多仓库源,切换市场是全局设置而非按插件。