Skip to content

fix(plugin-dev): 请求组织墙而企业包不可用时拒绝 init (ADR-0093 D5) (#5301) - #6053

Draft
qq9340100 wants to merge 1 commit into
mainfrom
claude/issue-5301-plugin-dev-d5-failfast
Draft

fix(plugin-dev): 请求组织墙而企业包不可用时拒绝 init (ADR-0093 D5) (#5301)#6053
qq9340100 wants to merge 1 commit into
mainfrom
claude/issue-5301-plugin-dev-d5-failfast

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes #5301

缺陷

DevPlugin 请求了有墙 tenancy posture(isolated / group)却加载不到企业 @objectstack/organizations 时,只打一条 logger.warn 就继续 boot。于是同一台机器上,同一个事实有两个相反的答案:

入口 请求 isolated、企业包缺失 结果
objectstack serve 拒绝启动(除非显式 OS_ALLOW_DEGRADED_TENANCY=1) 安全
DevPlugin(改前) warn 后继续 无墙服务流量,且没人显式同意过

ADR-0093 D5「请求了隔离就不得在没有隔离的情况下服务流量」是部署的性质,不是某一个入口的性质,所以 dev 装配路径欠同一个答案。#5262 让这条更容易被触发而不是更难:在它之前,只设 OS_TENANCY_POSTURE 的 dev 栈根本不进这个分支(那正是 #5262 本身的缺陷),修好读数之后它会进分支、会加载失败,然后正好走这条 fail-open 的路。

前提已按最新 origin/main(739f496)复核:dev-plugin.ts 的组织墙分支仍是单个 try + 只 warn 的 catch,无任何 hatch 闸。

改法

throw,不是 process.exit(1)

serve.ts 必须 process.exit,因为它那道闸嵌在会吞异常的 AuthPlugin try 里;DevPlugin库形态的装配插件,对宿主进程没有处置权,嵌入方(测试、脚本、父应用)有权 catch 它。而且它的 boot 链不吞异常——本 PR 已在当前 main 上复核该链仍成立:kernel.use() 只登记(kernel.ts:180),initPluginWithTimeout 只有 try/finally 没有 catch(kernel.ts:565),bootstrap() 的 catch 是 this.state = 'stopped'; throw error;(kernel.ts:405)。所以 throw 能真的中止 boot,与同文件 assertNotProduction() 的既有依据一致。

#4818 分两阶段,两种失败两种诊断

  • 阶段 1(import 失败 = 包缺席):OS_ALLOW_DEGRADED_TENANCY 生效。未设则拒绝 init,报文点名被请求的 posture 与全部出路;设了则照旧 warn 后降级继续,且该 warn 仍如实说明墙是 INACTIVE。判定用 resolveAllowDegradedTenancy()——serve.ts 同一个 resolver,两个入口对「显式同意」的定义因此不可能漂移(手写 === '1' 会让 true/on/yes 对 serve 有效、对 dev 无效,同一台机器同一份 .env)。
  • 阶段 2(construct / init 失败 = 包在、插件自己拒绝):hatch 不覆盖,一律中止。该 hatch 的含义始终是「这个能力缺席,我接受降级」,而不是「替我越过插件正在执行的闸」;放行会把插件的许可证/前置条件检查降格成一个环境变量。报文原样转述插件自己的说法(框架不解释、不猜原因),并明说这不是缺包问题,省掉一轮「去查安装」的排查。分类依据是哪一阶段抛的,刻意不是错误的形状——与 serve.ts 的理由相同。

阶段 2 在 DevPlugin 里比 serve.ts 多一处落点

serve 把插件交给 kernel.use(),其 Phase-1 循环会 rethrow init 失败,所以 serve 不需要为此写任何东西。而 DevPlugin 自己 init 子插件,那个循环刻意是 best-effort(记一条 error 继续,dev 栈才能在缺包时照常起)。对这一个子插件,best-effort 默认就是同一个 fail-open——墙没起来,进程照样服务流量。因此只让它单独例外,其余子插件的容错完全不变(有专门的回归用例钉住这条边界)。

若只镜像 construct 阶段而不管这处,本 PR 就会留下一个自己刚关掉的洞:包在、插件在 init() 里拒绝,依旧无墙继续跑。

测试

新增两个 suite,按「能不能诚实观察」切分:

  • dev-plugin-tenancy-failfast.test.ts —— 阶段 1。@objectstack/organizations 是云私有包、在本 workspace 里真实缺席,所以 import 真的失败、真实的 catch 分支真的跑,不 stub 任何被测对象。含:拒绝 init、报文点名 posture 与出路、hatch 放行后降级 warn 仍诚实、hatch 真值词表与 serve 一致、未调用 process.exit(这条断言能跑起来本身就是证明——process.exit(1) 会把 test runner 一起带走)、以及无墙 posture 完全不受影响。
  • dev-plugin-tenancy-mount-refusal.test.ts —— 阶段 2。「包在且拒绝」按定义无法在没有包的情况下观察,故 mock 仅提供 open-source workspace 不可能有的那一件东西(一个可解析的 @objectstack/organizations),被测的拒绝语义全部留在 dev-plugin.ts 里。含 construct 拒绝、init 拒绝、两者都不被 hatch 放行,外加一个正向对照(包在且健康则正常挂载)——没有它,整个文件可能只是靠「有墙 posture 一律抛」而通过,那就什么也没钉住。

已有的 #5262 suite 观察的是分支进入,它从 warn 那一行读这个信号;该路径现在 fail-fast,故其 beforeEach 显式设上 hatch 并写明理由——降级路径仍在同一行点名被请求的 posture,#5262 钉的信号一字未失。

pnpm --filter @objectstack/plugin-dev test    → Test Files 4 passed (4) / Tests 45 passed (45)
pnpm --filter @objectstack/plugin-dev typecheck → 干净

反向验证(方向为事前预测,非事后解释):预测「把 warn-only 块放回去,新增的拒绝类断言应转红」——即常规的红向,因为新用例断言的是旧代码从不产生的拒绝。实测:仅还原 dev-plugin.ts、保留全部新测试,Tests 11 failed | 34 passed,红的恰好是 11 条拒绝类断言;正向对照、best-effort 边界回归、无墙用例与整个 #5262 suite 全部保持绿。方向与预测一致。

消费半径已清扫:new DevPlugin( 全仓仅出现在本包内;examples/app-todoscripts/analytics-reconcile 等均显式 OS_MULTI_ORG_ENABLED='false'(single posture,不进分支),packages/qa/dogfood 为单组织 bootStack,packages/cli 的两个 organizations e2e 走的是 serve.ts

本地另跑通 CI ESLint job 内的家族闸:durability-log-levelstartup-registry-verdicterror-code-casingwildcard-fallthroughinit-service-contractroute-envelopeengine-double-contractservice-providerspublished-filesrole-worddoc-authoring 全绿,check:nul-bytes 与逐文件控制字符自扫描均干净,eslint packages/plugins/plugin-dev --no-inline-config 退出 0。

范围

⛔ 未改 packages/cli/src/commands/serve.ts(只读参照面)。已附 @objectstack/plugin-dev changeset,内含迁移说明:依赖静默降级的 dev 栈需显式设 OS_ALLOW_DEGRADED_TENANCY=1


Generated by Claude Code

DevPlugin 请求了有墙 posture 却加载不到企业 @objectstack/organizations 时,
只打一条 logger.warn 就继续 boot——同一台机器上,objectstack serve 对同一个
事实是拒绝启动。ADR-0093 D5 是部署的性质而非某个入口的性质,dev 装配路径欠
同一个答案。

改为 throw(不是 process.exit):DevPlugin 是库形态装配插件,对宿主进程没有
处置权;boot 链不吞异常(kernel.use 只登记、initPluginWithTimeout 不 catch、
bootstrap rethrow),与同文件 assertNotProduction() 的既有依据一致。

照 #4818 分两阶段:import 失败(缺包)受 OS_ALLOW_DEGRADED_TENANCY 管辖,
用的是 serve.ts 同一个 resolveAllowDegradedTenancy();construct/init 失败
(包在、插件自己拒绝)一律中止,hatch 不覆盖。阶段 2 在 DevPlugin 里多一处
落点——它自己 init 子插件的 best-effort 循环会吞掉 init 拒绝——故对这一个
子插件单独例外,其余子插件容错不变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbxm29qPKnLf44AbSxizqW
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 3:45pm

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-dev.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/kernel/services.mdx (via @objectstack/plugin-dev)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-dev)
  • content/docs/releases/v17.mdx (via @objectstack/plugin-dev)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-dev 没有 ADR-0093 D5 的 fail-fast:dev 栈请求了组织墙但企业包缺失时,只 warn 就继续跑无墙

2 participants