fix(plugin-dev,types): the production escape hatch stops being silent (#3900) - #4308
Merged
Merged
Conversation
…#3900) `DevPlugin.init()` refuses to run under `NODE_ENV=production` (ADR-0115 D6), and `OS_ALLOW_DEV_PLUGIN` overrides that refusal. As shipped, the override returned early with no output at all: the process ran the development assembly while every log line and the ready banner read like an ordinary production start. That reproduces, one level up, the defect the guard exists to close. The guard's own precedent says so — `OS_ALLOW_DEGRADED_TENANCY` boots degraded and brands it everywhere an operator looks, and `OS_ALLOW_DRIVER_CONNECT_FAILURE`'s contract is "logged loudly at startup". An escape hatch that says nothing leaves the operator's only evidence of a degraded state in an env var they may not have set themselves. The override now brands itself twice: a warning at `init()`, emitted before any assembly work so it survives an assembly step that later throws, and a repeat on the ready banner. Only hazards live for that configuration are named — the auth secret published inside the npm package (suppressed when the operator passed their own `authSecret`) and the in-memory driver with persistence off (suppressed when the `driver` toggle is off). The dev-admin seed is deliberately absent: `plugin-auth`'s `maybeSeedDevAdmin` is hard-gated to `NODE_ENV === 'development'` and cannot fire on this path, so warning about it would spend the attention the real hazards need. The flag also moves off a bare `process.env[…] === '1'` onto `resolveAllowDevPlugin()` in `@objectstack/types`, joining the `OS_ALLOW_*` family's shared truthy vocabulary next to `resolveAllowDegradedTenancy` / `resolveAllowDriverConnectFailure`. The strict comparison failed CLOSED on `OS_ALLOW_DEV_PLUGIN=true` — safe, but it reads to an operator as a broken flag. No change to the refusal path, which this issue re-verified end to end: `kernel.use()` only registers, `initPluginWithTimeout` does not catch, `bootstrap()` rethrows, and `os serve`'s outer handler prints the message and exits 1. The throw is genuinely fatal here, so it needs none of the `process.exit(1)` the tenancy guard required for sitting inside a broad catch. Verified against a real `LiteKernel` boot in all three states: refuses without the hatch, boots branded (init + banner) with it, and stays silent on an ordinary dev boot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTEKP96798WKamk2VCkAp
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…t the hatch (#3900) The docs-drift check flagged `content/docs/plugins/packages.mdx` as referencing the changed code. Its plugin-dev line described the guard's refusal and the escape hatch's existence, which is still true, but read as "the hatch turns the guard off" — the half this change replaced. It now says the override brands itself. `@objectstack/types`'s entry needed nothing: `resolveAllowDevPlugin` falls under the "degraded-boot helpers" it already lists. `releases/v17.mdx`'s only plugin-dev mention is the retired `DevPluginConfigSchema` cluster, unrelated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTEKP96798WKamk2VCkAp
os-zhuang
marked this pull request as ready for review
July 31, 2026 05:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3900.
先说结论:issue 里的三个 stub 已经不在了
#3900 报告的 fail-open stub(
verify()恒 admin、checkObjectPermission()恒 true、compileFilter()恒 null、maskResults()原样返回)在 issue 提出之后由 ADR-0115(#4126 + 后续整包 PR)整体退役了:DEV_STUB_FACTORIES整张表连同填充循环一起删除,没被任何 child plugin 占用的槽位就保持为空——和生产环境语义完全一致。dev-plugin.test.ts里那条"每个退役槽位必须保持为空"的墓碑测试就是防回归的闸门。生产护栏也已经存在(ADR-0115 D6):
init()在NODE_ENV=production时抛错,逃生舱是OS_ALLOW_DEV_PLUGIN。顺带把 issue 里对
serve.ts:1606那段的担心("throw 会被外层 catch 吞掉而静默降级启动")逐层核实了一遍,结论是这条路径上不会被吞:kernel.use()只做注册不跑init,initPluginWithTimeout不 catch,bootstrap()原样 rethrow,最后os serve的外层 handler 打印消息并exit(1)。所以这里的throw是真正致命的,不需要OS_ALLOW_DEGRADED_TENANCY那种process.exit(1)——那一处之所以必须用 exit,是因为它嵌在 serve 的宽泛 AuthPlugincatch里面,这一处不是。那还剩什么?逃生舱是哑的
issue 的建议里有一条没有被实现:"需要在类生产环境用它时,走一个显式的开关,并在启动横幅上打标。"
现状是:
OS_ALLOW_DEV_PLUGIN命中后直接return,一个字都不输出。进程跑着 dev assembly,而每一行日志、整个 ready 横幅,读起来和一次普通的生产启动毫无区别。这等于把护栏本来要堵的洞在上一层重建了一遍——操作者判断自己处于降级状态的唯一证据,是一个可能不是他本人设置的环境变量。而且这恰恰是这个 flag 自己的先例所禁止的:
OS_ALLOW_DEGRADED_TENANCY是"允许启动,然后在操作者会看的每个地方打标",resolveAllowDriverConnectFailure的契约原文是 "logged loudly at startup"。改了什么
1. 逃生舱现在会打标,而且打两次。
init()里一处(放在任何 assembly 工作之前,这样即使后续某一步抛错,警告也已经出去了——护栏被关掉的那次启动崩了,恰恰是操作者最需要知道护栏关着的时候),ready 横幅上再重复一次:只说对这份配置真实成立的危害:operator 自己传了
authSecret就不打印密钥那条,drivertoggle 关了就不打印数据那条。dev admin seed 故意不在列表里——plugin-auth的maybeSeedDevAdmin硬门在NODE_ENV === 'development',这条路径上根本触发不了,为一件不会发生的事发警告,花掉的是真正危害需要的注意力。2. flag 归队到
OS_ALLOW_*家族。 从裸的process.env[…] === '1'换成@objectstack/types里新增的resolveAllowDevPlugin(),和resolveAllowDegradedTenancy/resolveAllowDriverConnectFailure共用同一套 truthy 词汇表。原先的严格比较在OS_ALLOW_DEV_PLUGIN=true上是 fail CLOSED——安全,但在操作者眼里就是"这个开关坏了"。对操作者的 FROM → TO:
=1行为不变;=true(以及on/yes,大小写不敏感、忽略首尾空格)现在会生效,此前会被忽略并导致启动失败。这是朝着设置该 flag 的人本来就想要的方向放宽;falsy 值和无法识别的值仍然拒绝启动,未设置仍然是 fail-fast。如果谁在拿=true的失效当作"让护栏保持武装"的手段,请改成不设这个变量。验证
LiteKernel+ 完整DevPlugin,非 mock),三种状态逐一确认:不带 hatch 时拒绝启动;带=true时正常启动并在 init 与横幅两处打标;普通 dev 启动完全静默。resolveAllowDevPlugin的 env 测试。@objectstack/plugin-dev24 passed,@objectstack/types60 passed。pnpm build71/71 通过,pnpm lint干净。pnpm test:唯一失败的是@objectstack/service-datasource(sqlite-wasm 的mkdir /tmp/os-factory-wasm-*ENOENT)。已在 stash 掉全部改动后于干净的origin/main上复跑,失败完全一致——预先存在的问题,与本 PR 无关,单独跑该包时 178 passed。文档
ADR-0115 D6 追加了一条 amendment 记录这次修订(为什么静默的逃生舱是缺陷、为什么排除 seed、为什么
throw在这里够用),plugin-dev README 的 Production guard 一节补上了实际输出样例,changeset 携带完整的 FROM → TO。Generated by Claude Code