fix(seed): enforce Seed.env against the runtime environment - #4836
Conversation
`Seed.env` was authorable, defaulted and type-checked, but inert. `SeedLoaderService.load` filtered on the LOADER CONFIG's `env`, and none of the six call sites that build a `SeedLoaderRequest` (app boot, per-org replay, hot reload, package apply, draft publish, marketplace install) ever passed one — so `config.env` was always undefined, `filterByEnv` short-circuited, and `dataset.env` was never read at all. A dataset marked `env: ['dev']` seeded into production exactly as if it were marked `['prod']`. Resolve the environment in the loader — the one funnel every seeding path goes through — from NODE_ENV, the environment source this repo already uses everywhere (`os start` defaults it to production, `os dev` sets development, vitest sets test). No new env var, no new authorable key. An explicit `config.env` still wins. When the environment cannot be determined, stay permissive (seed everything, as before) but WARN, naming each environment-scoped dataset and the remedy. Fail-closed would also drop `env: ['prod']` datasets on a host that merely forgot to export NODE_ENV — silent data loss worse than the over-seeding it prevents. Skipped datasets are always named in an info log. Fixes #4704 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
范围外发现(Prime Directive #10)已单独立单,未指派,不在本 PR 修改:
Generated by Claude Code |
复核通过 —— ACCEPT,已标 ready 并送合并队列1. 修在漏斗处,不是修在六个调用点 —— 这是本 PR 最关键的判断,而且理由写进了代码。
一针见血。这个 bug 的成因就是调用点漏传;在调用点各补一遍,等于把洞推迟到第七个调用点。 2. 环境来源复用 同时接受 3. fail-open 的取舍我认可,因为伤害确实不对称。 环境判不出时( 而且 warn 只在确有数据集收窄了范围时才发( 4. 测试把"三种结局分开钉住"了。 这是本单最容易糊弄的地方:只证明「prod 下 dev 数据集不种」的话,一个「什么都不种」的实现同样能过。测试里那条 5. changeset 6. 顺带修掉的分裂值得表扬:解析出的环境同时绑定给 seed CEL 的 一处我核实过、结论是无需处理的我查了 范围外发现#4837(liveness 台账把「消费端存在读取代码」当作 live 证据,漏掉「无任何生产者传值」的死键)已单独立单、未指派、属 这条发现本身比它修的 bug 更有价值:台账核对了「消费端代码存在」,没核对「生产者是否真的传值」,于是这个洞在全绿 gate 下活了下来。同族佐证也找得很准—— Generated by Claude Code |
Fixes #4704
一、先核实:issue 描述完全属实
按 PM 要求,先验证再动手。三条都逐一在代码里核对过:
1.
filterByEnv的实际行为 —— 与 issue 引用的一字不差(packages/metadata-protocol/src/seed-loader.ts,修复前第 1355 行):2.
SeedLoaderConfig.env确实从未被填充 —— 全仓一共 六处 构造SeedLoaderRequest的地方,没有一处传env:packages/runtime/src/app-plugin.ts:974packages/runtime/src/app-plugin.ts:1042packages/runtime/src/app-plugin.ts:1234packages/runtime/src/domains/packages.ts:677packages/metadata-protocol/src/protocol.ts:7782packages/cloud-connection/src/marketplace-install-local-plugin.ts:1037所以
config.env恒为undefined→filterByEnv第一行直接返回全量 →dataset.env一次都没被读到。3. 补充一条 issue 没提、但值得记录的证据:
packages/spec/liveness/seed.json里env被标为"status": "live",证据指向seed-loader.ts:91(即消费端那一行),备注写着「filterByEnv drops datasets whose env list excludes the running environment」。这正是 AGENTS.md 第 10 条警告的那个陷阱 —— 「case标签不是 enforcement,要看调用点」。台账核对了「消费端代码存在」,没核对「生产者是否真的传值」,于是这个洞在绿灯下活了下来。(该文件在packages/spec/**,本轮零改动车道,我没碰;建议由 spec 车道把这条备注改成指向调用点的表述。)同族证据:
packages/runtime/src/seed-loader.test.ts:327早就有一个should handle environment filtering测试 —— 但它 显式传了env: 'prod'。所以过滤机制一直是好的,坏的只有接线。这也解释了为什么单测全绿而线上失效。二、修法:在唯一的漏斗处解析,而不是在六个调用点
环境解析放进
SeedLoaderService.load()—— 所有 seeding 路径的唯一必经之处。之所以不选「在六个调用点各自传
env」:这个 bug 的成因就是调用点漏传。在调用点补齐,只是把洞推迟到第七个调用点(将来任何新增 seeding 路径)重新打开。放在漏斗处,则新调用点结构上不可能漏掉 —— 这也是「让 AI 写的代码难以写错」的方向。环境来源复用仓内既有的那一个:
NODE_ENV。 没有新造环境变量,也没有新增可声明键。理由是它已经是全仓的事实标准,而且两条一方启动路径都会把它钉死:packages/cli/src/commands/start.ts:239 ——if (!localEnv.NODE_ENV) localEnv.NODE_ENV = 'production'(生产)packages/cli/src/commands/serve.ts:455 ——process.env.NODE_ENV = 'development'(os dev/serve --dev)test其余环境敏感行为(auto-DDL 关闭、api-registry 生产守卫、sqlite 降级、热重载 seeder)也都在读它。
映射:
production/prod→prod,development/dev→dev,test→test(大小写不敏感)。接受prod/dev简写,是因为NODE_ENV属于运维提供的第三方边界变量(Prime Directive #9 明确把NODE_ENV列为第三方例外),不是我们自己的元数据契约 —— 这与「不要在消费端加宽容 fallback」不冲突。优先级:宿主显式传的
config.env永远优先(这是文档化的 escape hatch,也是「以另一个环境的身份种数据」的唯一手段)。三、环境无法确定时:放行 + 响亮告知(fail-open)
这一条按 PM 要求给出理由。结论与 PM 倾向一致,但理由不止「向后兼容」:
决定性的一条是伤害不对称。 一刀切 fail-closed 不只会拦下
env: ['dev'],同样会拦下env: ['prod']—— 也就是说,一台只是忘了 exportNODE_ENV的生产主机,会静默丢掉本该种下的生产数据。那是数据缺失,比多种一批 dev 数据更糟,而且更难诊断。其次,不确定窗口在结构上很窄:两条一方启动路径都会钉死
NODE_ENV(见上),vitest 也会。真正落进这个窗口的,基本只有直接嵌入 kernel 的宿主。第三,它现在不再是静默的。当且仅当确实存在收窄了
env的数据集时,打一条warn,点名每个数据集、给出可接受的NODE_ENV取值、并指出config.env这条出路。没有任何数据集收窄时保持安静(不制造噪音)。诚实说明残留风险:一台
NODE_ENV未设的嵌入式生产宿主,仍然会种下env: ['dev']的数据 —— 但从此有一条明确的告警,而不是完全无声。这就是这次取舍的全部代价。被过滤掉的数据集一律点名(
info级)。用info而非warn,是因为在生产跳过 dev 数据集是声明所要求的正确行为,每次生产启动都 warn 属于狼来了;但它必须可查 —— 「我的 demo 数据怎么没了」应该是一行日志就能回答的问题。另外,解析出的环境同时绑定给 seed CEL 的
env(baseEvalCtx.env此前同样恒为undefined),避免出现「过滤器按 prod 走、表达式看到 undefined」的分裂状态。四、验收对照
四种情况分别有独立测试,且可相互区分(
packages/metadata-protocol/src/seed-loader-env-scope.test.ts,13 个用例):env: ['dev']在 dev 种、在 production 不种 —— 两条独立断言,外加一条produces a different result in production than in development显式断言两者结果必须不同(防止「全丢」或「全放」任何一种退化通过其中一条)。env/ 声明为 schema 默认值的数据集,在production/development/test三个环境都种 —— 行为零变化。warn点名 + 含NODE_ENV与config.env两处补救;而「无法确定但无人收窄」时保持安静,两者分别断言。distinguishes filtered, permissive-and-warned, and clean loads把三种结局的 (种下对象集合, 是否告警) 元组一次性钉死。反向验证:把修复代码 stash 掉、只保留测试跑一遍,13 个里 8 个失败,首条失败正是 issue 的复现 ——
expected [ 'account', 'demo_user' ] to deeply equal [ 'account' ](production 下 demo 数据仍被种下)。其余 5 个通过的正是向后兼容那几条,符合预期。五、范围
packages/metadata-protocol/src/seed-loader.ts+ 新增测试 + changeset。packages/spec/**零改动 ——Seed.env与SeedLoaderConfig.env的 schema 都无需改动,本次是纯接线修复。index.ts只导出SeedLoaderService,未动),故 changeset 定级 patch。packages/lint、skills/**、content/docs/**、content/docs/releases/,也未碰本轮其它 agent 的文件面。Generated by Claude Code