Repository navigation
feat(runtime): a host can supply the package-state store (createPackageStateStorePlugin); a refused write fails the enable/disable door with nothing changed - #31008
Conversation
Claude-Session: https://claude.ai/code/session_01L9qibvSvYrx9SiZMbSoehj Co-Authored-By: Claude <noreply@anthropic.com>
…d file default Claude-Session: https://claude.ai/code/session_01L9qibvSvYrx9SiZMbSoehj Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvSvYrx9SiZMbSoehj Co-Authored-By: Claude <noreply@anthropic.com>
…host store pin Claude-Session: https://claude.ai/code/session_01L9qibvSvYrx9SiZMbSoehj Co-Authored-By: Claude <noreply@anthropic.com>
…-lzvklr # Conflicts: # packages/runtime/src/domains/packages.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eb50027e7fd8e5253d833678672e741e97ad1a24 && git checkout eb50027e7fd8e5253d833678672e741e97ad1a24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8ae0ab9b0d77d56473e89dae9fe437778172dda8 8e94a195be3fd5a50537cca7592e4a67a889f2e6 && git checkout -B drift-repro 8ae0ab9b0d77d56473e89dae9fe437778172dda8 && git merge --no-ff 8e94a195be3fd5a50537cca7592e4a67a889f2e6
node scripts/docs-audit/affected-docs.mjs --json 8ae0ab9b0d77d56473e89dae9fe437778172dda8
|
|
domain:server#2 · os-tesla · session_01L9qibyvxbrLP4jm4PargoH 维护者速读
席位意见:可以落地。 席位核对ACCEPT · seat
|
Requested by Tesla · project thread
Fixes #30962
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
createPackageStateStorePlugin(store),启动种子和启用/停用/安装/删除各门都改读写这个存储;不加则仍是OS_HOME下的那个文件,字节不变。OS_HOME里的文件就没了,管理员在设置里关掉的模块会重新出现(cloud#3636,裁决「AAAA + 三条验收(推荐)」)。平台补上缺的宿主接缝,cloud 只需实现托管存储。packages/runtime/src/domains/packages.ts的启用、停用两门(先写存储的顺序与 503 分支);packages/runtime/src/app-plugin.ts的seedPersistedDisabledPackages与refuseLatePackageStateStore;新文件packages/runtime/src/package-state-host-store.ts。pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/package-state-host-store.test.ts,预期 11 个用例全绿。What changes
The seam (
packages/runtime/src/package-state-host-store.ts, exported from@objectstack/runtime).PackageStateStoreisloadDisabledPackageIds(environmentId)plussetPackageDisabled(environmentId, packageId, disabled), either may return a promise. A host addscreatePackageStateStorePlugin(store)to its kernel; that plugin (com.objectstack.package-state-store) registers the store as thepackage-state-storekernel service. This is the door a host already uses to hand the runtime anything:createStandaloneStackreturns apluginslist the host composes into its kernel, so no new top-level option was added.AppPluginlists the plugin name inoptionalDependencies(ADR-0116), so the store is registered before the boot seed reads it whatever slot the host puts it in;check:init-service-contractnow holds that init-time read to the declaration.Boot seed (
AppPlugin.seedPersistedDisabledPackages). Reads the host store when one is registered, else the file exactly as before (best-effort, warns). A host store whose read throws fails the boot with a named error instead of seeding nothing, which would bring every disabled module back enabled with nothing looking wrong (the ADR-0025 §3.5 M1 call the installed-plugin loader already makes).AppPlugin.start()refuses a boot that seeded from the file while a store is registered by then (a host registering under the service name from its own, unordered plugin), namingcreatePackageStateStorePluginas the fix.Doors (
domains/packages.ts). Every write goes through onewritePackageState, which looks the store up through the classified lookup (resolveServiceOrLoud), so a store factory that throws refuses the write rather than reading as "no host store".PATCH /packages/:id/enableand/disablenow write the store FIRST and switch the registry second. A host store that refuses: nothing changes in memory and the door answers503 SERVICE_UNAVAILABLE(the standard code, no new code), message openingPackage 'ID' was not disabled: the package-state store refused to record it, so nothing changed.The file default keeps its best-effortwarnand still switches.try; a host store's refusal there is logged aterrornaming what the next boot will read, the file's failure keeps the existingwarn.Tests
New
packages/runtime/src/package-state-host-store.test.ts, a realObjectKernel+ObjectQLPlugin+AppPluginand the realHttpDispatcheron the package doors, the store plugin composed AFTERAppPlugininuse()order:OS_HOMEstate directory is never created; a disable written in one boot is what the next boot (freshOS_HOME) seeds;SERVICE_UNAVAILABLEand the package stays enabled; enable answers 503 and it stays disabled; an unknown id still answers 404 and writes nothing;TypeError;Head
8e94a195(after mergingorigin/mainatc77dd17f):pnpm --filter @objectstack/runtime typecheck: exit 0 (check:test-typecheck: OK).pnpm --filter @objectstack/runtime test: Test Files 551 passed (551), Tests 8827 passed, 19 skipped.packages/runtime --no-inline-config: exit 0.Ablations (each once, through scripts/ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty; taken on 83af516, before the merge, which did not touch these lines)
A1, the refusal answered, removed (a refusing host store warns and disables anyway):
(The first A1 attempt used an empty
--replacement; the tool refused it as a non-landing mutation before running anything, and the leg was re-run with--delete.)A2, the old order back (registry switched before the store write, refusal kept):
A3, no host store and the file not written:
(Its first attempt was refused by the tool, the replacement text already occurring elsewhere; re-run with
--delete.)A4, no host store and a file failure made loud:
A5, the start-time refusal of an unordered store removed:
A6, the store plugin dropped from
AppPlugin.optionalDependencies:Acceptance notes
PackageStateStoreand addscreatePackageStateStorePlugin(store)to the hosted kernel.error; only enable and disable, which can write first, answer the refusal. (c) The 503 message embeds the store's own error text; the dispatcher's 5xx leak guard still replaces it if it looks like an internal leak.plugin-installation-store.ts(the localos plugin disable | enablecommands and the boot-time installed-plugin loader, ADR-0025 §3.5 M1) still reads and writes the file directly. That loop is local-first CLI state; a hosted container that wants it host-backed is a separate card.fyy60n) is adding a refusal pre-check to the enable door; whichever PR opens second mergesorigin/mainafter the first lands.packages/specedit, no new error code. Size: 6 files, +681 / −30.Verification
Derived gates (
dispatch-gates.mjs --commands, 63 families) pluscheck:init-service-contract,check:startup-registry-verdict,check:error-code-casing,check:route-ledger-census,check:authz-resolver,check:changeset-fixed,check:filter-alias-parityandcheck-platform-checklist.mjs --changed-from origin/main: all 71 exit 0 on8e94a195;dispatch-gates --ran: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN. Record:server-seat-2/r2026-11-06/out/30962-gates-ran-8e94a195.listin the project files.Notes on the gate run
check:test-source-aliaswas red on the new test (a clocked lazyimport('@objectstack/metadata')reached throughAppPlugin); fixed with a module-topimport '@objectstack/metadata'in the test, the gate's own prescription.check-plugin-teardown-shape --self-testandcheck:dual-build-cjs-loadsfirst answered exit 3 (prerequisite: pinned commit 621a487 absent from the shallow clone, and unbuilt dists); aftergit fetchof that commit and a fullturbo run build --filter='./packages/**', both exit 0.Generated by Claude Code