发现于 #5269(content/docs/deployment/tenancy-modes.mdx 文档核证)过程中,逐断言对码时撞到;本单只记录,不在该文档 PR 里改。
声称的契约
packages/types/src/env.ts:145-160 —— resolveTenancyPosture() 对无法识别的值抛错,错误文案自称:
Invalid OS_TENANCY_POSTURE="bogus". Expected one of: single, group, isolated (or the legacy alias 'multi' = 'isolated').
Refusing to boot rather than silently falling back to a posture with no organization wall.
content/docs/deployment/environment-variables.mdx:85 也照此写:「An unrecognized value refuses to boot」。
实际发生的顺序(静态追踪,origin/main @ c89d18c)
packages/cli/src/commands/serve.ts:1772 const tenancyPosture = resolveTenancyPosture(); 位于 AuthPlugin 那个大 try 内(更早的 kernel.use(new AuthPlugin(...)) @ serve.ts:1692 也会经 packages/plugins/plugin-auth/src/auth-plugin.ts:470 触发同一次解析)。
- 该
try 的 catch 在 serve.ts:1942-1948,只打印一句黄字 ⚠ AuthPlugin failed to load: <msg> 就继续。于是非法 posture 的第一次抛错被吞,启动继续进行,只是没有 plugin-auth。
- 这一点代码自己就写明了:
serve.ts:1816-1819 的注释说「this catch sits inside the broad AuthPlugin try below, which swallows errors — a throw would be caught and boot would silently continue degraded」,D5 闸门因此特意用 process.exit(1) 而不是 throw。posture 解析的抛错没有这层保护。
await runtime.start() @ serve.ts:2560 —— HTTP 服务已经起来并绑定端口。
printServerReady({ ..., tenancyPosture: resolveTenancyPosture() }) @ serve.ts:2698-2716 —— 同一个解析器第二次抛错,这次不在 AuthPlugin 的 try 里,落到外层 catch serve.ts:2764-2773,printError + this.exit(1)。
结论
退出码这一半是对的(最终 exit 1),但「refusing to boot」的另一半不成立:
- 进程在报错前已经 listen 过;
- 运维看到的第一条信号是「AuthPlugin failed to load」,把一个 env 拼写错误伪装成插件加载问题;
- 真正的
Invalid OS_TENANCY_POSTURE=... 只在最后由通用 printError 抛出,不是 D5 那种带修法清单的显式拒绝。
排查成本之外还有一个更实际的担忧:这条路径依赖「banner 一定会执行」这一点来兜底。任何让 printServerReady 不被执行的分支(或把 banner 的 posture 读取改成缓存值/容错读)都会把它变成「非法 posture 静默按 single 跑」——正是 env.ts 那句话要防的形态。
建议修法(不在本单实现)
在 serve 早期、任何 try 之外解析一次 posture 并显式 process.exit(1),与 ADR-0093 D5 闸门同一风格(有修法清单的 FATAL 文案),后续调用复用该值。
现状证据
packages/cli/** 下没有任何测试钉住这个行为:git grep -n "Invalid OS_TENANCY_POSTURE" packages/ 只命中 packages/types/src/env.ts:154。
发现于 #5269(
content/docs/deployment/tenancy-modes.mdx文档核证)过程中,逐断言对码时撞到;本单只记录,不在该文档 PR 里改。声称的契约
packages/types/src/env.ts:145-160——resolveTenancyPosture()对无法识别的值抛错,错误文案自称:content/docs/deployment/environment-variables.mdx:85也照此写:「An unrecognized value refuses to boot」。实际发生的顺序(静态追踪,
origin/main@ c89d18c)packages/cli/src/commands/serve.ts:1772const tenancyPosture = resolveTenancyPosture();位于 AuthPlugin 那个大try内(更早的kernel.use(new AuthPlugin(...))@serve.ts:1692也会经packages/plugins/plugin-auth/src/auth-plugin.ts:470触发同一次解析)。try的 catch 在serve.ts:1942-1948,只打印一句黄字⚠ AuthPlugin failed to load: <msg>就继续。于是非法 posture 的第一次抛错被吞,启动继续进行,只是没有 plugin-auth。serve.ts:1816-1819的注释说「this catch sits inside the broad AuthPlugin try below, which swallows errors — a throw would be caught and boot would silently continue degraded」,D5 闸门因此特意用process.exit(1)而不是 throw。posture 解析的抛错没有这层保护。await runtime.start()@serve.ts:2560—— HTTP 服务已经起来并绑定端口。printServerReady({ ..., tenancyPosture: resolveTenancyPosture() })@serve.ts:2698-2716—— 同一个解析器第二次抛错,这次不在 AuthPlugin 的 try 里,落到外层 catchserve.ts:2764-2773,printError+this.exit(1)。结论
退出码这一半是对的(最终 exit 1),但「refusing to boot」的另一半不成立:
Invalid OS_TENANCY_POSTURE=...只在最后由通用printError抛出,不是 D5 那种带修法清单的显式拒绝。排查成本之外还有一个更实际的担忧:这条路径依赖「banner 一定会执行」这一点来兜底。任何让
printServerReady不被执行的分支(或把 banner 的 posture 读取改成缓存值/容错读)都会把它变成「非法 posture 静默按 single 跑」——正是 env.ts 那句话要防的形态。建议修法(不在本单实现)
在
serve早期、任何try之外解析一次 posture 并显式process.exit(1),与 ADR-0093 D5 闸门同一风格(有修法清单的 FATAL 文案),后续调用复用该值。现状证据
packages/cli/**下没有任何测试钉住这个行为:git grep -n "Invalid OS_TENANCY_POSTURE" packages/只命中packages/types/src/env.ts:154。