Skip to content

fix(runtime): empty-env 早退跳过 disabled seed —— 禁用的包重启即复活 (#5047) - #5117

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-5047-empty-env-disabled-seed
Aug 4, 2026
Merged

fix(runtime): empty-env 早退跳过 disabled seed —— 禁用的包重启即复活 (#5047)#5117
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-5047-empty-env-disabled-seed

Conversation

@xuyushun441-sys

@xuyushun441-sys xuyushun441-sys commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #5047

问题

议题原文的定性(「enable/disable 只写内存」)在 PM 核实中已被推翻:持久化存在且刻意设计(package-state-store.ts$OS_HOME/package-state/{environmentId}.json),boot 回灌时序也自洽。本 PR 修的是核实过程中挖出的那个更窄的真 bug

AppPlugin.init 用「把 disabled 集合 seed 进 registry 的 initial-disabled 集」这一招,让所有注册路径(boot artifact 分解、sys_packages 重放、HTTP 安装)统一以 disabled 落位。但这个 seed 块位于 empty-env 早退之后:

init()
  ├─ installDefaultHookBodyRunner       ← 已因同样理由提前
  ├─ installDefaultActionBodyRunner     ← 已因同样理由提前
  ├─ installHookMetricsTiming           ← 已因同样理由提前
  ├─ wireAuthoredTranslationSync        ← 已因同样理由提前
  ├─ if (this.empty) return;   ← ✂️ 修复前 seed 在这条线之下
  └─ … seed …

而 empty env(artifact 无 app 载荷)恰恰是 hydration-only 场景:该环境的包全部来自 Phase 2 从 sys_packages 的重放,或来自 HTTP 安装。于是纯 DB 驱动的环境里 initial-disabled 集永远是空的,管理员禁用的包每次重启都以 enabled 复活,且全程无任何报错 —— 禁用状态写盘完全正确,只是没人读。

修法

把 seed 提到早退之前,与上面四个因同一理由被提前的调用并列;顺手抽成 seedPersistedDisabledPackages() 并把 rationale 写在方法上。

非 empty env 行为不变:manifest.register()(即分解发生的地方)在 init 末尾,seed 仍然先于它。seed 仍是 best-effort,在无 engine 的 kernel(metadata-only 一次性命令、mock-engine 测试)上静默降级。

测试(本单的一半)

这个 bug 能存在,直接原因是全仓零测试覆盖 package-state-store / setInitialDisabledPackageIds —— 往返的两头都没人断言。

packages/runtime/src/package-state-store.test.ts(12 例):写入/读取往返、重新启用移除条目、排序落盘、幂等、环境隔离(staging 的禁用不能影响 production)、OS_ENVIRONMENT_IDdefault 回退链、环境 id 路径消毒(../../etc/evil 不得逃出目录)、文件缺失 / JSON 损坏 / 非对象载荷一律降级为空集而非抛异常。

packages/runtime/src/app-plugin.disabled-seed.test.ts(7 例) —— 本 bug 的钉子,全部跑 kernel(LiteKernel + ObjectQLPlugin + empty-env AppPlugin)、状态文件:

  1. boot 后注册的包以 disabled 落位(hydration-only 路径);
  2. 只 seed 持久化过的 id,其他包照常 enabled;
  3. hydration 断言:把真 registry 交给PackageServicePlugin.start(),从 sys_packages 重放的包以 disabled 落位(SQL 层用 hydration.test.ts 同款 execute 替身,registry 与 boot 的是同一个);
  4. 重新启用后清除持久化状态,包回到 enabled;
  5. 无持久化状态时 empty env 照常 boot;
  6. 非 empty env 既有行为不变(bundle 声明的包仍以 disabled 落位);
  7. 无 engine 的 kernel 上静默降级、仍能 boot。

为第 3 例给 @objectstack/runtime 加了 @objectstack/service-packagedevDependency + vitest alias(dev-only,与既有的 @objectstack/service-job 同款先例):这样重放路径断言的是真实 hydration 代码,而不是它的复刻品。

反向验证(把 seed 挪回早退之后,重跑同一批测试):

FAIL  … a package registered after boot lands DISABLED — the hydration-only path
-   "enabled": false,     -   "status": "disabled",
+   "enabled": true,      +   "status": "installed",

FAIL  … a package replayed from sys_packages by PackageServicePlugin lands DISABLED
-   "enabled": false,     -   "status": "disabled",
+   "enabled": true,      +   "status": "installed",

Test Files  1 failed | 1 passed (2)
     Tests  2 failed | 17 passed (19)

正好红在两个 empty-env 用例上,非 empty env 用例保持绿 —— 钉子钉在了它该钉的位置。

验证

命令 结果
pnpm --filter @objectstack/runtime test Test Files 84 passed (84) / Tests 1150 passed (1150)
pnpm --filter @objectstack/runtime typecheck 绿(tsc --noEmit 无输出)
pnpm --filter @objectstack/service-package test 4 passed
eslint(改动文件) 干净
check:init-service-contract / check:engine-double-contract / check:published-files / check:type-check-coverage / check:durability-log-level / check:service-providers 全绿

Changeset:.changeset/empty-env-disabled-package-seed.md(@objectstack/runtime patch),写明用户可见后果。

本 PR 不修(PM 评论已记录为债)

  • 状态跨两种介质(disabled 在 OS_HOME 文件、包在 DB),容器 / 多副本 / 临时文件系统下会漂移 —— 那是 package-state-store.ts rationale 的 ADR 级取舍;
  • DELETE /packages/:id 清理 sys_packages不清理 disabled 条目 → 同 id 重装会静默以 disabled 回来;
  • sys_packages 无 lifecycle 列(raw-SQL 表,无 spec 定义),统一到 DB 需 schema 变更。

…arly return (#5047)

`AppPlugin.init` seeds the registry's initial-disabled set from
`<OS_HOME>/package-state/<environmentId>.json` so every registration path
installs operator-disabled packages disabled. That seed ran AFTER the
empty-env early return, and an empty env (no app payload in the artifact) is
exactly the hydration-only scenario: its packages all arrive later from
`sys_packages` replay or an HTTP install. Result: on DB-driven environments
the initial-disabled set stayed empty and disabled packages came back enabled
on every restart.

Move the seed above the return, next to the hook/action body runners and the
authored-translation sync that are already hoisted for the same reason, and
extract it into `seedPersistedDisabledPackages()` with the rationale attached.

Tests (the blind spot that hid this — `package-state-store` and
`setInitialDisabledPackageIds` had zero coverage repo-wide):
- `package-state-store.test.ts` — round trip, per-environment isolation,
  missing/corrupt file degradation, env-id sanitization.
- `app-plugin.disabled-seed.test.ts` — real LiteKernel + ObjectQLPlugin +
  empty-env AppPlugin over a real state file, asserting post-boot
  registration and the REAL `PackageServicePlugin` `sys_packages` replay both
  land disabled; plus the non-empty env's existing behavior.

Reverse-verified: with the seed moved back below the return, exactly the two
empty-env cases fail (`installed` / `enabled: true`) and the non-empty case
stays green.

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

vercel Bot commented Aug 4, 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 4, 2026 5:22am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/m labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

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

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

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.

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 4, 2026 05:24
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 78adc2e Aug 4, 2026
26 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-5047-empty-env-disabled-seed branch August 4, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime /packages domain 持久化不对称:enable/disable/uninstall 直写 in-memory registry,install/update 却走 protocol service 落 sys_packages

2 participants