Skip to content

fix(plugin-dev,types): the production escape hatch stops being silent (#3900) - #4308

Merged
os-zhuang merged 2 commits into
mainfrom
claude/plugin-dev-stub-fail-open-heggws
Jul 31, 2026
Merged

fix(plugin-dev,types): the production escape hatch stops being silent (#3900)#4308
os-zhuang merged 2 commits into
mainfrom
claude/plugin-dev-stub-fail-open-heggws

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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() 只做注册不跑 initinitPluginWithTimeout 不 catch,bootstrap() 原样 rethrow,最后 os serve 的外层 handler 打印消息并 exit(1)。所以这里的 throw 是真正致命的,不需要 OS_ALLOW_DEGRADED_TENANCY 那种 process.exit(1)——那一处之所以必须用 exit,是因为它嵌在 serve 的宽泛 AuthPlugin catch 里面,这一处不是。

那还剩什么?逃生舱是哑的

issue 的建议里有一条没有被实现:"需要在类生产环境用它时,走一个显式的开关,并在启动横幅上打标。"

现状是:OS_ALLOW_DEV_PLUGIN 命中后直接 return一个字都不输出。进程跑着 dev assembly,而每一行日志、整个 ready 横幅,读起来和一次普通的生产启动毫无区别。

这等于把护栏本来要堵的洞在上一层重建了一遍——操作者判断自己处于降级状态的唯一证据,是一个可能不是他本人设置的环境变量。而且这恰恰是这个 flag 自己的先例所禁止的:OS_ALLOW_DEGRADED_TENANCY 是"允许启动,然后在操作者会看的每个地方打标",resolveAllowDriverConnectFailure 的契约原文是 "logged loudly at startup"。

改了什么

1. 逃生舱现在会打标,而且打两次。 init() 里一处(放在任何 assembly 工作之前,这样即使后续某一步抛错,警告也已经出去了——护栏被关掉的那次启动崩了,恰恰是操作者最需要知道护栏关着的时候),ready 横幅上再重复一次:

⚠ DEV ASSEMBLY UNDER NODE_ENV=production (OS_ALLOW_DEV_PLUGIN is set) — the boot
  guard was explicitly overridden. This process is running the DEVELOPMENT
  assembly, which is not hardened for production traffic (ADR-0115 D6).
    • Auth secret is the default published inside @objectstack/plugin-dev. It is
      public, so anyone can mint a session this stack accepts. Pass `authSecret`
      explicitly.
    • Data goes to the in-memory driver with persistence disabled — every record
      is lost when this process exits.

只说对这份配置真实成立的危害:operator 自己传了 authSecret 就不打印密钥那条,driver toggle 关了就不打印数据那条。dev admin seed 故意不在列表里——plugin-authmaybeSeedDevAdmin 硬门在 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 启动完全静默。
  • 新增 14 条 plugin-dev 测试(打标内容、operator 自带 secret 时不误报、横幅重复、普通 dev 启动无噪声、truthy/falsy 词汇表各 5 例)+ 3 条 resolveAllowDevPlugin 的 env 测试。@objectstack/plugin-dev 24 passed,@objectstack/types 60 passed。
  • pnpm build 71/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

…#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
@vercel

vercel Bot commented Jul 31, 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 Jul 31, 2026 5:09am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-dev, @objectstack/types.

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

  • content/docs/plugins/packages.mdx (via @objectstack/plugin-dev, @objectstack/types)
  • 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.

…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
os-zhuang marked this pull request as ready for review July 31, 2026 05:34
@os-zhuang
os-zhuang merged commit 2a37694 into main Jul 31, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/plugin-dev-stub-fail-open-heggws branch July 31, 2026 05:34
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-dev 的 stub 全部 fail-open(verify() 恒 admin、checkObjectPermission() 恒 true、compileFilter() 恒 null)且无任何生产环境护栏

2 participants