装载管线与 guard
guard 是装载器的一部分,是装载流程中的一个阶段,不是独立插件,也不是需要被询问的常驻服务。一切单元——根结构、普通插件、Mod、数据包、资源包——装载时都经过同一个 guard。
相关页面:架构总览、大插件与小插件、根插件名单(RPM)、装载原因码。
唯一触发点
guard 的触发器在且仅在一个扩展单元被装载时。
| 时机 | 行为 |
|---|---|
| 启动 | 装载根结构一次;与普通单元走同一个判定函数 |
| 运行 | 装载四类可变单元 N 次;每次装载判定一次 |
| 操作热路径 | 不进入;不会在每次渲染、每次读文件、每次服务调用时被询问 |
| 卸载之后 | 残留由孤儿机制处理;guard 已无拒绝力,不承担清理 |
由此得到三个性质:不是热路径上的扼流点;不持有状态副本;判定失败只影响当次装载。
七段管线
装载请求
│
├─ ① 读取 / 解析 / 机械校验
├─ ② 签名与信任
├─ ③ RPM / 契约 / IR
├─ ④ 能力 / 预算
├─ ⑤ 分类与写边
├─ ⑥ 装载
└─ ⑦ 装载后核验
├─ 拒绝 ──────────→ 原因码 + 报告 + 通知
├─ 待用户裁决 ────→ 装载器挂起并驱动界面
└─ 允许 ──────────→ 继续下表按判定职责分层;judge 是一个纯函数,函数内部各项检查的先后顺序以 src/core/kernel/guard/judge.ts 为准。
| 阶段 | 做什么 | 主要代码 | 失败码 |
|---|---|---|---|
| ① 读取 / 解析 / 机械校验 | 打开包来源,读取 pack.json,校验格式版本、稳定 ID、semver、kind、文件表、单文件入口与归档哈希;逐文件计算 SHA-256 | src/core/kernel/inspect.ts、manifest.ts、packs/reader.ts | LOADER_MANIFEST_INVALID、LOADER_VERSION_UNSATISFIED |
| ② 签名与信任 | 验签覆盖 pack.json;区分签名有效、失败、未签名;装载器每次判定前经宿主绑定读取一次信任快照,再判四态与同意 | inspect.ts、guard/judge.ts、loader.ts | GUARD_SIGNATURE_INVALID、GUARD_UNSIGNED_IN_RELEASE、GUARD_PUBLISHER_REVOKED;待裁决见下文 |
| ③ RPM / 契约 / IR | 根结构核对入口哈希与契约;普通单元核对 buildChannel、targets、数据包的 ir: ^N,并核对 Mod 改写对象所属大插件的契约 | guard/judge.ts、src/shared/contract.ts | GUARD_ROOT_ENTRY_MISMATCH、GUARD_CONTRACT_INCOMPATIBLE、GUARD_IR_UNSUPPORTED |
| ④ 能力 / 预算 | 必需能力缺失即拒绝;可选能力缺失产生降级提示;T3 与隔离专用能力只发给隔离运行单元;预算取声明值与默认上限的较严者 | guard/judge.ts、宿主 capabilities() | GUARD_CAP_UNAVAILABLE、GUARD_BUDGET_EXCEEDED |
| ⑤ 分类与写边 | 由内容与清单计算数据包 / 资源包 / 插件 / Mod;核对 registry 落点、实现位与机制插槽落点、policy、保留命名空间、解码路径、安全关键 UI、重名与替换位 | guard/judge.ts、src/shared/slots.ts | GUARD_CLASSIFICATION_MISMATCH、GUARD_SLOT_UNDECLARED、GUARD_POLICY_CLOSED 等 |
| ⑥ 装载 | 按获准能力建立 isolate 作用域;交出按清单裁剪的 ctx;数据包按 registry 挂层并可整体回滚;代码单元执行单文件 ESM;隔离 codec 只登记代理条目 | src/core/kernel/loader.ts | 装载失败按具体阶段返回 |
| ⑦ 装载后核验 | 实际 apply 失败,或声明过的写边没有真正发生,即卸载并记录 | loader.ts 的 #postCheck | LOADER_POST_CHECK_FAILED、LOADER_UNLOADED |
① 读取与机械校验
包来源有三种形态:builtin(内存文件表)、directory(目录读取器)、zip(随机读取)。ZIP 只接受 store 与 deflate,拒绝分卷、ZIP64、加密、路径穿越、重名项与越界内容。
代码单元只允许一个打包后的 ESM 入口:code/ 下只能有清单 entry 指向的单个文件。多出文件或入口不在文件表内即 LOADER_MANIFEST_INVALID;两端宿主都不提供包内模块之间的相对 import。
归档哈希取全部文件「相对路径 → SHA-256」表的 JCS 后再哈希,文件时间与重打包方式不参与。核验通过后,读取器在每次交付字节前复制并与私有哈希快照比对;核验后内容变化时,签名单元以 GUARD_SIGNATURE_INVALID 拒绝,未签名单元以 LOADER_MANIFEST_INVALID 拒绝。
② 签名与信任
签名核验只回答「这个包是否由它声称的发布者发布、是否被第三方动过」,不回答「代码是否安全」。四态必须严格区分:
| 情形 | 处置 |
|---|---|
| 签名有效且在官方清单内 | 直接装载 |
| 签名有效但不在清单内 | 提醒用户,同意后装载(TOFU) |
| 签名核验失败 | 拒绝装载 |
| 未签名 | release 下:代码单元直接拒绝(GUARD_UNSIGNED_IN_RELEASE),数据包与资源包进入未签名批准流程;debug 下:代码与数据单元均允许,但须取得批准(GUARD_UNSIGNED_UNAPPROVED) |
信任快照由装载器在每次判定前读取一次,只含与待判单元相关的记录,作为参数传给 guard。guard 不自行访问信任存储、不缓存快照、不跨判定复用。
③ RPM、契约与 IR
根结构的判定只核对两件事:RPM 中该 id 的契约与入口哈希。普通单元还要核对 targets 能否匹配当前根插件契约;数据包与资源包必须声明 ir: ^N。契约兼容窗口是 [current-1, current]。
④ 能力与预算
能力按单元清单与宿主可用性判定。必需能力缺失即拒绝,且相关对象不存在;可选能力缺失产生降级提示;T3 与隔离专用能力只发给隔离运行单元。清单外的单元若申请 T2 能力或 trusted 写入目标,还需要用户逐项同意;清单内发布者免这一步。
预算默认上限为归档 1 GiB、单文件 256 MiB、文件数 100,000。单元可以声明更小值,但不得超过默认上限;内容事实超过预算即拒绝。
⑤ 分类与写边
分类只看两类事实:包内容与清单声明。
| 事实 | 结论 |
|---|---|
| 无代码,内容面写入 registry | 数据包 |
| 无代码,表现面提供资产 | 资源包 |
| 有代码,无写边 | 插件 |
| 有代码,有写边 | Mod |
数据包只能写 RPM 登记的 registry;保留命名空间或既有 closed 条目不可写;数据包与资源包都不得触碰解码路径,也不得包含安全关键 UI 的保留命名空间。代码单元若自报档位与计算档位不符,即拒绝。代码单元还必须声明 persistence,缺失按 fail-closed 拒绝。
实现位落点必须在 slots 中声明,且 policy 不为 closed;机制插槽同理。登记重名、replace 位已被占用、翻译器未在隔离环境运行,都会在装载前拒绝。发布者名还要经过规范化、字符集与同形检测。
⑥ 装载
代码单元拿到的是按清单裁剪过的上下文:intercept 的名字须在 writes.intercepts 内;isolate 域内 provide 的名字须在 writes.isolateProvides 内;isolate 域之外的 provide 一律报错。未声明的调用抛出 SlotError。
获准能力只在单元自己的 isolate 域里提供,未获准的能力在该域里不存在。数据包的 registry 层按单元整体挂载,任一层失败即回滚已挂的层。隔离 codec 的入口字节只作 Worker 源码,主 realm 不执行;装载器以该单元身份登记代理条目。
⑦ 装载后核验
装载后核验比对「声明过的写边」与「实际发生的写边」。声明了却没有发生,同样判为不符并卸载;apply 抛错或 fiber 进入失败状态也卸载。依赖尚未出现的软依赖单元,会在其激活时补做核验。
装载完成前收到同一单元的卸载请求,本次装载在下一个检查点以 LOADER_UNLOADED 结束,不发装载完成通知。
guard 判定清单
启动时,RPM 完整性由启动层核对内嵌常量、根结构判定核对入口哈希,两者共同完成;运行期单元判定不重复核对整份清单。下表把这条启动链与单元判定放在一起列出。
| 判定项 | 依据 | 拒绝条件 |
|---|---|---|
| RPM 完整性 | manifestHash 与产物内嵌常量 | 不一致 |
| 根插件身份 | rootPlugins[].entryHash | 不一致 |
| 合约兼容 | rootPlugins[].contract | targets 无法匹配兼容窗口 |
| IR 版本 | 数据包/资源包的 ir | 缺失或不兼容 |
| 实现位落点 | slots[] | 注册到未声明的实现位 |
| 机制插槽落点 | slots[]、services[] | 写入未声明或 closed 的目标 |
| policy 准入 | 目标声明的 policy | 写入 closed 目标 |
| capability 门禁 | 单元声明的 caps | 必需能力不存在 |
| 预算声明 | 单元预算与内容事实 | 超过默认上限或声明上限 |
| 隔离要求 | 单元类型与落点 | 翻译器/编解码器未按要求隔离 |
| 发布渠道 | 编译期 buildChannel | 仅在 debug 构建下判定调试类单元;release 中该代码路径必须不存在 |
| 签名四态 | 签名事实、官方清单、信任快照 | 核验失败;release 下未签名代码单元 |
| 发布者名唯一性 | 信任存储(不经 ctx) | 同名异钥 → 待用户裁决 |
| 名称规范化 | 框架内置实现 | 控制字符、零宽字符、视觉同形、混合脚本 |
发布渠道一项需要特别说明:release 的等价拒绝由宿主侧承担——不注册 handler、不监听端口,而不是在 JS 侧运行期判断。
guard 自身约束
| 编号 | 约束 |
|---|---|
| G1 | guard 位于 Kernel 内,属于焊死根集合,其哈希登记于 RPM |
| G2 | 没有机制插槽;服务 policy 全为 closed——安检器不可被扩展 |
| G3 | 判定是纯函数:输入 RPM、待装载单元、运行时状态与信任快照,输出裁决;不持有状态副本 |
| G4 | fail-closed:判定不确定、参照缺失、哈希读不到,一律拒绝 |
| G5 | 只暴露只读的 guard:report;仅装载器与本框架内置 UI 可见,不对四类可变单元 provide 或 inject |
| G6 | 拒绝必须给出原因码,但不得向被拒单元泄露比对参照细节 |
| G7 | 裁决为三态:允许 / 拒绝 / 待用户裁决 |
| G8 | 「待用户裁决」由装载器挂起并驱动 UI;挂起状态归属装载器,不归属 guard |
guard 只返回裁决,不写审计。 原因码记录、报告与通知由装载器写入;guard:report 是只读视图,条目只含单元 ID、原因码与时间,不含信任参照值。
guard 不做什么
| 不做 | 原因 |
|---|---|
| 运行期接口拒绝(IPC、端口、调试路由) | 不是扩展单元装载,属于宿主边界 |
| 缓存判定结果 | 会退化为状态副本 |
| 维护依赖账本 | 禁止集中式依赖管理器 |
| 参与操作热路径 | 会成为扼流点 |
| 卸载后的残留处置 | 由孤儿机制承担 |
| 自证其自身 | 由宿主侧产物校验与 RPM 覆盖 |
| 负责调试接口本身 | 调试接口存在与否由宿主决定;guard 只负责装载期的调试类单元判定 |
待用户裁决不是最终结果
待用户裁决只表示「当前参照不足以自动放行」,不是拒绝,也不是允许。装载器会挂起新来者并驱动信任页;用户在界面内作出答复后,装载器重新读取信任快照并重新判定。
| 裁决码 | 含义 |
|---|---|
GUARD_PUBLISHER_UNLISTED | 签名有效,但发布者不在官方清单内 |
GUARD_PUBLISHER_KEY_CONFLICT | 同名发布者出现了不同密钥 |
GUARD_UNSIGNED_UNAPPROVED | 未签名包尚未被批准,或归档哈希已变化 |
GUARD_CONSENT_REQUIRED | 单元申请的权限或 trusted 写入目标尚未获同意 |
答复后仍判为待裁决,说明参照不完整,按 fail-closed 以 GUARD_INDETERMINATE 拒绝;用户拒绝则为 LOADER_USER_DECLINED。冲突弹窗必须展示两个公钥的完整指纹、各自时间戳,以及是否来自官方清单;名单内一方必须明确标注。
拒绝矩阵
原因码按类别分为:清单与版本(LOADER_*)、签名与信任(GUARD_SIGNATURE_*、GUARD_UNSIGNED_*、GUARD_PUBLISHER_*)、落点与 policy(GUARD_SLOT_*、GUARD_POLICY_CLOSED)、契约与 IR、能力与预算、分类与隔离,以及装载生命周期(LOADER_USER_DECLINED、LOADER_POST_CHECK_FAILED、LOADER_UNLOADED)。完整定义见装载原因码。
| 用例 | 期望 |
|---|---|
篡改 manifestHash | 拒绝启动 |
| 某根插件入口哈希不符 | 拒绝启动 |
注册到 slots 未声明的实现位 | 拒绝注册并给出原因码 |
Mod 的 targets 与根插件契约不匹配 | 拒绝装载 |
写入 closed 目标 | 拒绝写入,保留原值 |
| 必需 capability 不存在 | 拒绝,且相关对象不存在 |
| 翻译器未在隔离 realm 中运行 | 拒绝装载 |
| release 下装载调试类单元 | 拒绝;真正的硬拒绝位置在宿主 |
| 资源包试图影响解码路径 | 拒绝 |
| 资源包包含安全关键 UI 的保留命名空间 | 拒绝 |
| 签名核验失败 | 拒绝装载 |
| release 下未签名代码单元 | 拒绝装载 |
| debug 下未签名代码单元 | 允许,但须取得批准 |
| 同名异钥 | 弹窗并挂起新来者 |
| 发布者名含零宽字符、控制字符或视觉同形 | 拒绝 |
| 已装载单元尝试在 RPM 之外挂载新子树 | 记为观测,不宣称已阻止 |
已装载单元使用 eval 或 new Function 植入代码 | 无法防护;仅要求可观测与留痕 |
| 用户拒绝待裁决单元 | LOADER_USER_DECLINED |
| 装载后核验不符 | 卸载并记录 LOADER_POST_CHECK_FAILED |
同 realm 限制
必须明确:guard、Kernel、大插件、小插件与 Mod 运行在同一个 JavaScript realm 内。guard 是装载流程中的判定阶段,不是沙箱,也不提供技术隔离。
它能做的是:在装载时按声明与 RPM 机械判定落点、policy、契约、能力与预算;在装载后核对声明过的写边是否真的发生。它不能做的是:阻止同一 realm 内的代码经 ctx.root 等途径绕过约定、修改对象原型、劫持模块加载或读写同源存储;也不能阻止用户修改自己本地的产物。
因此,拒绝矩阵里「eval 不可防护」与「RPM 之外挂载只能观测」不是遗漏,而是明确的边界。真正的硬边界在别处:宿主持有的 capability、不可信代码的独立 realm、远端信任链。翻译器与二进制编解码器的隔离运行属于独立 realm 的专门边界,不推广为「所有插件都被沙箱隔离」。