背景
复测 #2926 / #2909 期间修复了三个 kernel:ready 生命周期时序缺陷后,对其余 kernel:ready 消费者做了一轮预防性审计,专找同一类隐患——「在 kernel:ready 里读取别的插件同期 bootstrap/seed 产出的数据、且注册顺序不保证消费者晚于生产者」。
结论:0 CONFIRMED、7 SAFE、1 PLAUSIBLE。本 issue 记录唯一的 PLAUSIBLE 项。它是一个独立的、预存的、低紧急度边角,与 #2926/#2909 的缺陷线无关,属 ADR-0093 D6(membership backfill)领域。
现象
plugin-auth 的一次性 membership backfill 是 member-less 用户的唯一兜底网,在极端时序下可能漏掉经种子插入的用户。
根因链:
-
种子用户绕过 per-create 兜底。app 种子经 raw engine.insert(system context)插 sys_user,绕过 better-auth 的 user.create.after hook —— 因此每建一个用户就跑一次的 reconcileMembership 网对种子用户不触发。它们唯一的网就是下面这条一次性 backfill。
-
backfill 是一次性 kernel:ready hook,无种子完成回调。
packages/plugins/plugin-auth/src/auth-plugin.ts:604 起 ctx.hook('kernel:ready', …) → backfillMemberships(packages/plugins/plugin-auth/src/reconcile-membership.ts:158)。读 sys_user + sys_member,把 member-less 用户绑到默认组织。跑完即止,种子后续完成不会重触发。
-
种子会在超预算时脱离到后台。
packages/runtime/src/app-plugin.ts:849-858:inline seed 只对 OS_INLINE_SEED_BUDGET_MS(默认 8s)预算 Promise.race;超时即 warn 后继续在后台跑(仅挂 .catch),溢出到 kernel:ready / kernel:listening 之后。AppPlugin 注册在 auth/security 之前,是唯一能让 kernel:ready 消费者观察到「部分种子」状态的现实路径。
净结果:大 bundle 的种子若 >8s 预算而脱离后台、在 backfill 的 kernel:ready 之后才插完用户 → 这些用户在单组织 auto 模式下保持 member-less(登录时 active org 为空)。
自限条件(为什么定为低紧急度)
- 需三条同时成立:种子超 8s 预算脱离 +
membershipPolicy: 'auto' 单组织 + 用户仅来自种子(非交互注册)。
- 重启即自愈:下次 boot 的 backfill 会补上;backfill 幂等。
- multi-org 与
invite-only 模式自动 no-op,不受影响。
- 可用
OS_SKIP_MEMBERSHIP_BACKFILL=1 显式退出(与本缺陷正交)。
修复方向(需设计取舍,勿顺手改)
- ⚠️ 仅迁到
kernel:bootstrapped 锚点不够:后台种子能 outlast kernel:listening,换锚点只收窄窗口不封死。
- 候选真解(择一,值得单独讨论):
- 种子完成回调重跑 backfill —— 种子 promise settle 后再跑一次 backfill(即便已在后台);最贴合"兜底网"语义。
- 种子用户不绕过 membership 兜底 —— 让 raw-insert 的
sys_user 也过 reconcileMembership(或种子通道显式补 membership),消除对一次性 backfill 的依赖。
- 不脱离种子 —— 但这会重新引入种子阻塞 kernel 启动的问题(8s 预算正是为规避它而设),不推荐。
关联
Filed from the preventive kernel:ready ordering audit following #2926/#2909. Triage only — not yet scheduled.
Generated by Claude Code
背景
复测 #2926 / #2909 期间修复了三个
kernel:ready生命周期时序缺陷后,对其余kernel:ready消费者做了一轮预防性审计,专找同一类隐患——「在kernel:ready里读取别的插件同期 bootstrap/seed 产出的数据、且注册顺序不保证消费者晚于生产者」。结论:0 CONFIRMED、7 SAFE、1 PLAUSIBLE。本 issue 记录唯一的 PLAUSIBLE 项。它是一个独立的、预存的、低紧急度边角,与 #2926/#2909 的缺陷线无关,属 ADR-0093 D6(membership backfill)领域。
现象
plugin-auth的一次性 membership backfill 是 member-less 用户的唯一兜底网,在极端时序下可能漏掉经种子插入的用户。根因链:
种子用户绕过 per-create 兜底。app 种子经 raw
engine.insert(system context)插sys_user,绕过 better-auth 的user.create.afterhook —— 因此每建一个用户就跑一次的reconcileMembership网对种子用户不触发。它们唯一的网就是下面这条一次性 backfill。backfill 是一次性
kernel:readyhook,无种子完成回调。packages/plugins/plugin-auth/src/auth-plugin.ts:604起ctx.hook('kernel:ready', …)→backfillMemberships(packages/plugins/plugin-auth/src/reconcile-membership.ts:158)。读sys_user+sys_member,把 member-less 用户绑到默认组织。跑完即止,种子后续完成不会重触发。种子会在超预算时脱离到后台。
packages/runtime/src/app-plugin.ts:849-858:inline seed 只对OS_INLINE_SEED_BUDGET_MS(默认 8s)预算Promise.race;超时即 warn 后继续在后台跑(仅挂.catch),溢出到kernel:ready/kernel:listening之后。AppPlugin 注册在 auth/security 之前,是唯一能让kernel:ready消费者观察到「部分种子」状态的现实路径。净结果:大 bundle 的种子若 >8s 预算而脱离后台、在 backfill 的
kernel:ready之后才插完用户 → 这些用户在单组织auto模式下保持 member-less(登录时 active org 为空)。自限条件(为什么定为低紧急度)
membershipPolicy: 'auto'单组织 + 用户仅来自种子(非交互注册)。invite-only模式自动 no-op,不受影响。OS_SKIP_MEMBERSHIP_BACKFILL=1显式退出(与本缺陷正交)。修复方向(需设计取舍,勿顺手改)
kernel:bootstrapped锚点不够:后台种子能 outlastkernel:listening,换锚点只收窄窗口不封死。sys_user也过reconcileMembership(或种子通道显式补 membership),消除对一次性 backfill 的依赖。关联
primary-bu-projectionbackfill(hook 先于 backfill 接线、种子写入也触发 recompute,无缺口)· 三个 trigger 插件(注册 provider 非读种子,绑定顺序无关)·auto-org-admin-grant(live reconcile hook 自愈)·authored-translation-sync(metadata:reloaded幂等重跑)· audit/reports 建表(生产者非消费者)。Filed from the preventive
kernel:readyordering audit following #2926/#2909. Triage only — not yet scheduled.Generated by Claude Code