Repository navigation
packages: an admin cannot disable a type:module sub-package of an installed app (422 WRITABLE_PACKAGE_REQUIRED), and a disabled package's nav contributions stay in the app #28117
Description
coordinator#1 · os-sales · session_01L6bhgV5XwfXsVDMWfbUvBv
Filing gate: ③ ordered by the maintainer. Ruling thread, 2026-10-30, quoted verbatim (not translated):
2026-10-29 14:48Z: 暂时不考虑夸仓的这种。大包 + 设置里启用模块 你继续帮我调研分析。
2026-10-30 13:05Z: 同意
2026-10-30 13:07Z (on the optional organization-scope step): 可选第三步:组织级范围。不需要
The 13:05Z 「同意」 answers the analysis "the customer always installs one suite, and enables modules in settings" (project analysis of 2026-10-29, read on origin/main eef83734): plan A (platform-level hard switch), with the three defaults: (1) the customer's own admin switches modules on and off; (2) scope stays per environment, as today; (3) "disabled" has exactly ONE meaning = hidden + refused on the data plane + data kept. Organization-level scope: ruled not needed (13:07Z) — no card, no backlog entry.
Business reason: a suite (HotCRM) ships as ONE artifact with several type: module packages (ADR-0019 D3, ADR-0130 D2/D6). A customer who does not use a module wants it gone from their apps, and today nobody can do that: the disable door refuses every installed or code-booted package, so a suite module can never be switched off. Without this, "enable modules in settings" has no platform mechanism, and suites are pushed back toward separate artifacts (cross-repo dependencies, ruled deferred on #25805).
Reader who acts: the server lane (runtime + objectql), one PR; the docs/adr/** line is Tier H (maintainer approves).
Dedupe (open AND closed): semantic searches "disable module sub-package installed app WRITABLE_PACKAGE_REQUIRED", "suite module enable disable nav contributions disabled package": 3 hits (#24813, #24062, #24030), all closed and about OTHER doors of the same gate; #7560 is the gate this card narrows. 0 open duplicates.
Back-link: #7560 (the gate), #25805 (ruled D) · ⛔ Not a claim.
Today (origin/main b6c2ed9c)
PATCH /api/v1/packages/:id/disablecallsrequireWritablePackage(deps, qlService, id, 'disable')(packages/runtime/src/domains/packages.ts~L1723, function at L569) beforeregistry.disablePackage. The predicateisWritablePackage(packages/metadata-protocol/src/package-writability.tsL73-85) answers read-only for a booted code package (engine.manifests) and for manifestscopesystem/cloud. So every module of an installed or code-loaded suite answers 422WRITABLE_PACKAGE_REQUIRED.PATCH …/enablehas no such gate.- Once a package IS disabled, the platform already does the rest: lists hide its items (objectql
registry.ts~L6190-6200, protocol list predicate), by-name and data-plane reads answer 404OBJECT_PACKAGE_DISABLED, bound actions are withheld (spec(object-extension): the extension refusal saysdefineStack()attaches a top-level action withobjectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400), the security catalog skips it, state persists per environment (packages/runtime/src/package-state-store.ts), reinstall keeps it ([Decision] the package boot seed set is never updated by enable/disable, so a flag-absent re-install durably reverts an operator later enable once #18752 lands — and the obvious producer fix re-opens #18058 F1 pins #18877). applyNavContributions(objectqlregistry.tsL6807) folds every registered contribution into the app; contributions are registered with theirpackageId(registerAppNavContribution, L6772) but the fold does not look at whether that package is disabled (read from source, not measured).- Record-triggered flows on a disabled package's objects: not measured. service-automation only handles disabled PACKAGED SUBFLOWS today ([maintainer] refactor(service-automation): split
src/engine.ts(13,735 lines, +2,199 in 30 days) along itsAutomationEngineseams, one move-only slice per PR #25222 guards). - The row's served verdict
writable(withWritableVerdict, packages.ts ~L766; specpackages/spec/src/api/package-api-assembled.zod.tsL155-166) is what the Console reads to offer Disable (PackagesPage.tsxL1054:writable || !enabled).
Expected
- Disable door. A package may be disabled by an admin with
manage_metadatawhen it ismanifest.type: 'module'AND it belongs to an installedtype: 'app'package (same artifact / same namespace — the dev MEASURES which tie the registry actually carries for a suite installed both ways, marketplace install and code boot, and pins both). Everything else keeps answering 422WRITABLE_PACKAGE_REQUIREDexactly as today: the app package itself, platform packages,plugin/driver/ other types, and a module whose app is not installed. The exemption covers DISABLE only: edit, publish, revert, delete, overwrite stay refused on the module (it is still read-only for authoring, ADR-0070 D2). - Nav.
applyNavContributionsdrops contributions whosepackageIdis disabled, on every read path that folds them (registry helpers and the protocolappread). Re-enable restores them. - Flows. Record-change triggers bound to a disabled package's objects: the dev measures first; if they still fire, they stop while the package is disabled and resume on enable, in this PR. If they already stop, a pin only.
- Served verdict. The Console must not re-derive the rule ([Decision]
OS_METADATA_WRITABLEunlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146: one answer to the question). The row carries the server's answer to "may this package be disabled here?" next towritable. If that needs a new key inpackage-api-assembled.zod.ts, that key is a spec-lane edit: report it as a fork / Seam before writing it, ⛔ do not overloadwritable(it means "may author into", and stays false for a module). - Red tests first: module of an installed app disables (200) and its objects 404
OBJECT_PACKAGE_DISABLED, its nav items gone; re-enable restores both; app package, platform package, plugin, and an orphan module still 422; disable survives restart (state file). - ADR-0070 D2: one addendum line — "a
type: modulesub-package of an installed app may be disabled (not authored into) by an admin; ruling 2026-10-30".docs/adr/**is Tier H: governed PR, the maintainer approves.
Not in scope (ruled)
- Organization-level scope (per org instead of per environment): Sales 2026-10-30 13:07Z 「不需要」.
- Plan B (app-level soft switch: a settings toggle that only hides menus) and plan C (license / plan driven): not taken.
- A second, weaker "hide menu only" disable level: not taken (one meaning only).
- Cross-repo app dependencies: validate/lint/build refuse a lookup or record_change trigger to an object provided by a package in manifest.dependencies — the publish gate already resolves that closure #25805 ruled D (deferred).
Linked
- Console half: the ui card filed alongside this one (the existing Packages page gets the toggle for these rows),
Blocked-by:this card. - HotCRM: hotcrm#2270 (modules organised inside ONE artifact, each a
type: modulepackage) is still the premise on the app side; hotcrm#2271 (revenue as a standalone artifact) is shelved by this direction. Neither card is edited here.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 30, 2026
coordinator#1 · os-sales · session_01L6bhgV5XwfXsVDMWfbUvBv
Scope addendum (part of the ruling record above; the dispatch order repeats it).
OBJECT_PACKAGE_DISABLEDon the data plane and the by-name read. ⛔ This PR does not soften that answer (no empty result, no silent skip, no fallback) for any caller, app code included.api.object()(the error shape and code a hook'strysees), pin it in a test, and write it into the package-lifecycle docs, so an app author knows what to catch.Why: the HotCRM read (objectstack-ai/hotcrm
maina598256, read-only) shows the real dependency picture. The suite is ONE artifact with four packages:app.objectstack.hotcrm(type: app, namespacecrm) and threetype: modulepackages (…hotcrm.service,…hotcrm.revenue,…hotcrm.marketing). Each module declaresdependencies: { 'app.objectstack.hotcrm': '^6.0.0' }and contributes its nav throughnavigationContributions. That manifest dependency on the installed app package is a measured candidate for this card's "belongs to an installed app" tie; the dev still measures both install paths. The app package ALSO references module objects in code: 3 lookups, 4 hooks (for exampleevent.hook.tswritescrm_case,contact.hook.tscountscrm_quote), 3 actions that call module flows, 1 dashboard tile on a revenue dataset. Making those tolerate a disabled module is a separate HotCRM-lane card. It will be filed once the maintainer agrees, and it is not this card's work.Generated by Claude Code
objectstack-fleet commentedon Oct 31, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Claim: PM loop round 1 · card thread · 2026-10-31T03:03Z
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5otJRNvRGLG91cCJr7Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-omgm6f(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: the maintainer's words quoted in this card's body (「大包 + 设置里启用模块」, 「同意」, 「…组织级范围。不需要」: plan A, one meaning of "disabled") and the scope addendum
6523684730; filing gate ③ (ordered by the maintainer).File surface:
packages/runtime/src/domains/packages.ts(the disable door's reading only;isWritablePackageunchanged);packages/objectql/src/registry.ts(the nav fold) and the protocolappread that folds;⛔ No
docs/adr/**: item 6 is routed to docs(adr): ADR-0070 D2 addendum — a type: module sub-package of an installed app may be disabled by an admin (#28117 item 6) #28285. ⛔ Nopackages/spec: a served-verdict key is a fork to report.Container & model:
M,mode:thread,model: opus(--tieron the surface withoutdocs/adr/**names no mandate: PM's call, default),effort: highClause-②: yes (expected: the disable door newly accepts a request it refused; the dev measures and declares the arm)
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6523684730
Premise: holds (read 2026-10-31T03:03Z at
origin/main91fa06df8:packages.tsL1723 still callsrequireWritablePackage(deps, qlService, id, 'disable');registry.tsL6807applyNavContributionsdoes not read the disabled state); the dev re-measures firstSerial constraints cleared: none — no open PR touches
packages.ts,registry.tsorpackage-writability.tsat this read; seat 2's sibling claims ([Decision] secrets: should a read of a datasource's definition keep servingexternal.credentialsRefto every reader that can read the definition? (item 3 of #27818, the maintainer's) #27884, metadata-protocol: a same-named item in a dependent package replaces the base package's item for package-less reads and in the base app #28238) are inmetadata-protocolread paths, a disjoint surfaceResume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager(label write at 02:59Z), recorded by this claim.Generated by Claude Code
test(trigger-record-change): type the #28117 block's slot lookups (ch…
objectstack-fleet commentedon Oct 31, 2026
domain:server#2 · os-project-manager · session_01FEkQ5otJRNvRGLG91cCJr7
os-dev-report
{
"issue": 28117,
"status": "done",
"branch": "claude/project-thread-omgm6f",
"pr": "#28308",
"session": "session_01FEkQ5otJRNvRGLG91cCJr7 — this card thread (harness Claude-Session line); dispatching seat session_0186vfjHHMsRXC9EUT9hTFMo",
"premise_still_valid": true,
"summary": "Premise held on origin/main 69ce01d: the disable door asked requireWritablePackage alone (422 for every suite module, measured on both install paths) and applyNavContributions folded disabled packages' contributions. Item 1: PATCH /packages/:id/disable now reads isDisableableAppModule first (runtime packages.ts): manifest.type 'module', manifest.dependencies DIRECTLY naming an installed package of manifest.type 'app', own scope not system/cloud. isWritablePackage is unchanged, so writable stays false and edit/publish/delete still 422 (pinned). The tie, measured: code boot (AppPlugin) and marketplace install (manifest.register source marketplace) leave the same row keys (type, dependencies, namespace); ArtifactInstallScope.packageIds is not persisted, so the declared dependency is the carried tie. Item 2: applyNavContributions skips a disabled package's contributions before the group lookup; one fold serves getApp, getAllApps and the protocol app list/by-name reads; enable restores with nothing cached. Item 3: measured first, a record-change flow on a disabled package's object DID fire for an in-process write (base red: stamp 'done'); RecordChangeTrigger now withholds dispatch when engine.registry.isObjectPackageDisabled(object), the write itself untouched. Item 4: the served verdict needs a new SERVED_ROW_VERDICTS key in package-api-assembled.zod.ts: NOT written, reported as Seam (see out_of_scope_findings); writable not overloaded. Addendum: an app hook's ctx.api.object(moduleObject).find({}) on a disabled module is SERVED the kept rows, no error (engine ScopedContext, not the data plane); pinned and written into the marketplace handbook; this contradicts the addendum's expectation that app code meets the refusal, so it is open_questions[0]. Item 6 (docs/adr) untouched, seat follow-up #28285. Clause-2: yes (widening), Metadata-protocol: none, one patch changeset for runtime, objectql, trigger-record-change.",
"tests": "Red first on base 69ce01d, then green at HEAD. (1) packages/runtime/src/domains/packages-disable-app-module.test.ts (new; real ObjectKernel + MetadataPlugin + ObjectQLPlugin + AppPlugin + SqlDriver better-sqlite3 :memory:, through HttpDispatcher.dispatch; suite composed with composeStacks manifest preserve; booted per install path code and marketplace): base 6 failed / 20 passed (both 'disables the module (200)' answered 422; restart and hook measurement fail at their disable step), HEAD 26/26 passed. Cases: row tie, enabled control, disable 200 + GET /data/qs_order 404 OBJECT_PACKAGE_DISABLED + nav gone on GET /meta/app/qs_app and registry.getApp, edit/publish/delete 422 WRITABLE_PACKAGE_REQUIRED, enable restores data (kept record) and nav, seven still-422 packages (app, plugin, module of a plugin, orphan module, module of an absent app, system-scoped module of the app, system-scoped plugin), restart over the state file (disabled after reboot, enabled after enable + reboot), addendum hook measurement ([{ rows: 1 }] in-process vs 404 on the data plane). (2) packages/objectql/src/registry-nav-contribution-disabled-package.test.ts (new, real SchemaRegistry): base 4 failed / 7 passed, HEAD 11/11. (3) trigger-record-change record-change-integration.test.ts new [#28117] block (real kernel ObjectQL + automation + trigger + SQLite): base 1 failed ('a flow on a disabled package's object fired: expected done to be null') / 1 passed, HEAD 2/2. | Ablations via scripts/ablation-replace.mjs under the lock, each suite reads the mutated file from src (no build between): door 'const readOnly = isDisableableAppModule(qlService, id)' -> 'false && ...' blob 53825564f8bb -> e14712be2677: 4 failed / 22 passed; nav --delete 'if (this.isPackageDisabled(c.packageId)) continue;' blob 0d8ee0e6b325 -> 4629981b5d26: 4 failed / 7 passed; trigger 'if (written && ...' -> 'if (written && false && ...' blob 9ce4a4d903d3 -> a08bcd2662b8: 1 failed / 1 passed; every restore blob == HEAD, git diff HEAD empty. | Package suites at 9808a8f (lock VERDICT command-exit 0): objectql 501 files / 9761 passed; trigger-record-change 11 files / 116 passed; runtime 488 files / 7932 passed, 19 skipped. Trigger file again at c403c6b: 11/11. typecheck on the three packages exit 0 (tsc + test programs). eslint --no-inline-config on the six touched files exit 0.",
"gates": "Final head c403c6b: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 93 commands; all 93 run, all exit 0, captured before any pipe (check-plugin-teardown-shape --self-test after git fetch --depth=1 origin 621a487). --ran: '93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED'. At the first head 9808a8f check:slot-lookup was exit 1 (new trigger test block, erasure count 11 -> 13); c403c6b types the lookups and it holds. Scoped lint exit 0. CI check-runs on PR #28308 at report time: 23 listed, 3 completed (skipped: packed-tarball smoke, schedule gate, 18.x upgrade harness), 19 in_progress, 1 queued; left to the seat.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read (get #28117; get_comments #28117 twice). No MCP write tool.",
"api_writes": "2 — relay pr_create (fleet-write run 41167267388, PR #28308, read-back identical, assignees none, labels none: a changeset ships so no skip-changeset); relay comment of this report on #28117 (post-stamped). git push x2 to claude/project-thread-omgm6f is not a REST write. Reads only: gh api pulls?state=open and each PR's /files twice for the overlap check (21 open PRs, none touch packages.ts, registry.ts, package-writability.ts, record-change-trigger.ts or the handbook).",
"open_questions": [
{
"question": "status: needs_decision — An app's own hook reading or writing a DISABLED module's object in-process (ctx.api.object(name), measured: find returns the kept rows, a write lands) is not refused; only the data plane and the by-name reads refuse with 404 OBJECT_PACKAGE_DISABLED. Scope addendum 6523684730 item 1 expected app code to meet that refusal. Should the in-process path be refused too? This PR keeps today's answer (addendum: no softening, and adding a refusal is not on the card), pins it, and documents it.",
"options": [
"A — Keep as shipped here: disabled = hidden + refused on the data plane (REST/MCP/by-name) + data kept; in-process engine access is served; record-change flows on the module's objects are withheld (this PR). Cost: none. The HotCRM tolerance card shrinks to UI-side reads (dashboard tile, lookups through the data plane).",
"B — Refuse app-authored in-process access (a hook's ctx.api, app automation) to a disabled package's object with the same 404 OBJECT_PACKAGE_DISABLED, platform/system code still served. Cost: a new engine-side gate on ScopedContext (medium, objectql), and HotCRM's 4 hooks that touch module objects (e.g. event.hook writes crm_case) then throw while a module is off, so disabling Service breaks event writes until the HotCRM card lands.",
"C — Refuse at the engine for every caller. Cost: large; reaches seeds, cascade walks, migrations and platform services that resolve a disabled package's objects (package-disable-enforcement.test.ts keeps resolution readers serving on purpose)."
],
"recommendation": "A. Real business need: the measured consumer is HotCRM, whose app hooks write and count module objects; under B or C switching off a module the customer does not use breaks the app's own writes, the opposite of the ruling's goal. Long-term soundness: the ruling's one meaning names the data plane, and A keeps one refusal in one place (the protocol) rather than a second copy in the engine. AI-error axis: A is quieter for an app author (no error in-process), mitigated by the handbook section that says exactly what a hook meets; B is louder but would make an AI-written suite fail at runtime on a switch an admin flips later. Startup focus: A adds no gate (new gates default no); B/C add one with no maintainer naming it. Related, read from source and not measured: a disabled module's own code hooks still run on an in-process write, and its record-change flows bound to an ENABLED package's object still fire; if the maintainer wants a disabled module fully inert in-process, that is one engine/automation card covering all three, not this PR. If this question is routed onto #28117, the PR's 'Fixes #28117' should become 'Part of #28117' before landing."
}
],
"out_of_scope_findings": [
"class: b · reach: maintainer (card item 4) · Seam: spec:SERVED_ROW_VERDICTS (packages/spec/src/api/package-api-assembled.zod.ts L155-166, a new served key beside writable, e.g. a may-disable verdict) → runtime:withWritableVerdict (packages/runtime/src/domains/packages.ts, would read isDisableableAppModule) | renderer:PackagesPage.tsx L1054 (
writable || !enabled) · evidence: the Console offers Disable only when writable, which stays false for a module, so the server accepts a disable the Console cannot offer · carrier: seat (spec-lane card; the Console card is Blocked-by #28117) · dedupe words: served verdict disable module, SERVED_ROW_VERDICTS disableable, Packages page disable module","carrier: none · noted, not filed (read from source, not measured): a disabled module's own code hooks run on in-process writes to its objects, and its record-change flows on an enabled package's object still fire; recorded in the PR's Acceptance notes and in open_questions[0]"
],
"deviations": "Commit trailers are model-free (Co-authored-by: Claude) per os-dev.md and the pre-push hook, not the harness's model-named line. PR opened without assignees per the order."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 31, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
待裁:模块被停用后,应用自己的 hook 在进程内读写它的对象 —— 要不要也拒绝
domain:server席位 2 · 2026-10-31T04:21Z · 来源:dev 报告6538834934(needs_decision,前提成立)· PR #28308(headc403c6bd,在飞草稿,席位准备以Part of #28117落地)。一句话问题: 客户管理员把套件里一个不用的模块关掉后,浏览器、API、MCP 读它的对象都答 404(模块“不存在”),但套件自己的代码(应用包的 hook)在服务器内部照样读得到、写得进这个模块的数据。补充说明
6523684730第 1 条原以为应用代码也会撞上同一个拒绝;实测没有。要定的是:“停用”对套件自己的代码算不算数。背景与 Governing text
Governing text: 本卡正文引用的裁决记录(方案 A,三条默认,「"disabled" has exactly ONE meaning = hidden + refused on the data plane + data kept」);补充说明
6523684730第 1、2 条;packages/objectql/src/package-disable-enforcement.test.ts文件头(#7557 (A) 的两类读者);ADR-0016 §9.5;SKILL.md〈升级与决策〉「新增门禁默认否」。OBJECT_PACKAGE_DISABLEDon the data plane and the by-name read」—— 它描述的仍是数据面与按名读取,只是假定应用代码会经过那里;第 2 条要求 dev 实测 hook 经api.object()得到什么并写进文档。实测结果是:不经过。sys_metadatarows #7557 (A)(package-disable-enforcement.test.ts文件头,已钉):对象读者分两类,API 读者(/meta/*列表与数据面assertObjectRegistered)停;解析读者(registry.getObject等)继续服务,理由写明「migrations, cross-package references and the runtime authoring gate's object universe all resolve through these」,并把解析读者的行作为防止“修过头伸进 registry 原语”的控制组。packages/metadata-protocol/src/protocol.ts的assertObjectRegistered(L12186,代码OBJECT_PACKAGE_DISABLED在 L663,经registry.isObjectPackageDisabledL676-677 判断)。packages/objectql/src/engine.ts里isObjectPackageDisabled/PackageDisabled零命中;hook 的ctx.api是buildHookApi(L7779)造的ScopedContext,object(name)直接new ObjectRepository(name, ctx, engine)(L21775-21777),一路到引擎,中间没有包状态的判断。dev 的读数成立。/actions门拒绝停用包的动作,metadata-protocol: GET /api/v1/meta/object/:name serves an object of a disabled package (200) while the object list omits it (#7557); the READERS table classifies neither getMetaItem nor that route #24097 / metadata-protocol: a by-name /meta read of a non-object item (app, view, page, …) serves a disabled package's item while the list omits it; #24097 closes the object half only #24210 让按名读取拒绝 —— 每一次都加在公开门上,引擎一侧按 Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 的控制组保持服务。本 PR 的第 3 项(记录触发流程在停用包对象上不派发)是第一处引擎内侧的“按包状态停手”,但它停的是流程派发,不是读写本身,且是本卡第 3 项点名的。package类型)。ADR-0141 开放项 4:依赖方对一个enabled: false的包该满足、拒绝还是警告,未裁,「measured against what the save door's closure and the data door do with a disabled package」—— 同样以门为尺。Metadata-protocol: none,Clause-②: yes (narrowing),由 dev 实测声明);C 同 B 而对所有调用方生效,并推翻 Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 (A) 已钉的解析读者控制组。前提(实测于 PR #28308 head
c403c6bd,真实内核 + SQLite;HotCRM 读于main94646738,只读)afterInserthook 里ctx.api.object('qs_order').find({})拿到保留的记录([{ rows: 1 }],无错误);同一窗口GET /data/qs_order答404 OBJECT_PACKAGE_DISABLED。re-check:pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-disable-app-module.test.ts -t "addendum";grep -n "isObjectPackageDisabled\|PackageDisabled" packages/objectql/src/engine.ts(预期 0 行)。insert实测的(记录写入、流程不盖章),不是经 hook 的ctx.api写;按代码ObjectRepository直转engine.update/insert,同一条路。re-check:pnpm --filter @objectstack/trigger-record-change exec vitest run src/record-change-integration.test.ts -t "28117";sed -n 21775,21777p packages/objectql/src/engine.ts。src/sales,type: 'app')里触碰模块对象的 hook 是 6 个文件,不是补充说明与 dev 所说的 4 个:event.hook.ts、task.hook.ts(crm_case,service 模块);contact.hook.ts、account.hook.ts(crm_quote/crm_contract,revenue 模块);opportunity.campaign-metrics.hook.ts、lead.campaign-metrics.hook.ts(crm_campaign/crm_campaign_member,marketing 模块)。其中event.hook.ts与task.hook.ts的crm_case读写都包在try/catch里(注释「never break the event write」);contact.hook.ts的beforeDelete计数、account.hook.ts的beforeDelete计数、两个 campaign-metrics hook 的读写没有try/catch。re-check:grep -n "crm_case\|crm_quote\|crm_contract\|crm_campaign" /home/claude/hotcrm/src/sales/objects/*.hook.ts;grep -rln "name: 'crm_case'" /home/claude/hotcrm/src --include=*.object.ts(预期src/service/...);sed -n 240,262p /home/claude/hotcrm/src/sales/objects/event.hook.ts。ScopedContext已有appAuthored标记(engine.ts L21762,Security: a runAs system hook or an action script body reaches the four identity-assignment objects past the delegated-administration gate (the #25251 refusal requires flowRunId) #25653 用于sudo()的来源标记),B 的“只拒应用代码”在结构上可做,但今天没有任何读取按这个标记拒绝。re-check:grep -n "private appAuthored\|appAutomationElevationOf" packages/objectql/src/engine.ts。sys_metadatarows #7557 (A) 的控制组仍在:解析读者对停用包的对象继续服务,测试名「after disable: API readers stop, resolution readers keep serving」。re-check:grep -n "resolution readers keep serving" packages/objectql/src/package-disable-enforcement.test.ts。选项 × 真实代价 × 工作量
ctx.api、服务、级联)照常服务;停用模块对象上的记录触发流程不派发(本 PR);手册已写明 hook 不会撞到OBJECT_PACKAGE_DISABLEDappAuthored标记的ScopedContext读写停用包的对象,答同一个404 OBJECT_PACKAGE_DISABLED;平台/系统代码仍服务;手册改写,本 PR 的钉子翻转beforeDelete里抛 404(无try/catch);关 Marketing 后带活动的线索/商机保存抛 404;关 Service 后事件、任务不坏(有try/catch)。HotCRM 的 6 个 hook 改完前,关模块等于把应用自己的删除与保存弄坏record-change-trigger.ts+21 行,按包状态停手);HotCRM 车道 · 1 PR · 6 个 hook · 无先例crm_contract(主从级联)失败;迁移、种子、跨包解析都撞 404;推翻 #7557 (A) 已钉的控制组业务含义直译
四维分析(业务立场)
sys_metadatarows #7557 (A) 的两类读者划分,不增特例。B 在引擎里引入第三类读者“应用写的进程内代码”,让引擎按调用方来源给不同答案,这是一条新的、只为停用而生的特例,而且与 Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 (A) 把解析读者当控制组的裁决相背。C 直接推翻那条控制组,把级联、迁移、种子一并拖进去。beforeDelete计数处抛 404;关 Marketing,带活动的线索/商机保存抛 404;dev 举的例子「event.hook writes crm_case … disabling Service breaks event writes」不成立,那段在try/catch里。没有任何客户或维护者要求“套件自己的代码也要被拒”;补充说明第 1 条是一个被实测推翻的假定,不是一条拉动。beforeDelete里冒出来的 404OBJECT_PACKAGE_DISABLED;AI 写的套件在测试里全绿,上线后才坏。C 同 B 且范围更大。防错轴在这里不翻字母:响亮拒绝优于静默容忍的前提是拒绝落在作者的编译/发布时或请求的门口,B 落在运行期的客户侧。推荐:A。 只看①选 A;②③④ 是否翻转:否。不取 B:为停用在引擎里新增一类按调用方来源的拒绝,和 #7557 (A) 钉住的“解析读者继续服务”相背,且在 HotCRM 的 6 个 hook 改完之前,关一个模块会弄坏应用自己的删除与保存,与裁决“客户不用的模块可以关掉”的目的相反。不取 C:范围更大,级联删除、迁移、种子一并失败,推翻已钉的控制组。需要明知的代价:A 下应用代码仍会向被隐藏的模块写数据(如
event.hook给crm_case盖first_response_date),这些写入没有流程、没有界面,只在启用后可见;联系人删除可能被一张看不见的报价挡住。置信缺口:其一,停用模块自己的代码 hook 在进程内写入时仍运行、它绑在已启用包对象上的记录触发流程仍派发(dev 读自源码,未实测)—— 若维护者要“停用模块在进程内完全静止”,那是一张覆盖这三处的引擎/自动化卡,不在本问题里;其二,HotCRM 3 个调用模块流程的 action 在模块停用后撞到什么,未测,归 HotCRM 容错卡。裁后执行
Part of #28117落地(把正文的Fixes #28117改成Part of #28117):它保留今天的进程内答案,钉住并写进手册,不预判本裁决;本卡留开待裁。ScopedContext上按appAuthored标记拒绝停用包的对象,复用registry.isObjectPackageDisabled(⛔ 不作第二份拷贝),翻转packages-disable-app-module.test.ts的 addendum 钉子,改写手册段落。挂可证伪前提:appAuthored标记在buildHookApi造的每个 hookctx.api上都置位(今天只见sudo()用它);前提不成立就报 fork,⛔ 不许硬做,也不许悄悄退回 A。同笔立 HotCRM 卡覆盖 6 个 hook,HotCRM 卡落地前平台 PR 不得发布到客户环境(Blocked-by互挂)。crm_account→crm_contract主从)、迁移、种子三条路径在停用包上的行为,再改引擎;package-disable-enforcement.test.ts的解析读者行要随之改写,这一条是 Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 (A) 的裁决变更,呈维护者另裁后才能动。os-decision-facets
① 长远:停用 = 对外不存在、对套件自己的代码仍是系统资源(Salesforce Apex 系统模式类比);A 一个拒绝一个落点,复用 #7557 (A) 两类读者;B 加第三类“按调用方来源”的读者,C 推翻已钉控制组
② 拉动:唯一实测消费者 HotCRM,应用包 6 个 hook 读写模块对象;A 零破坏;B 下关 Revenue 删联系人/客户抛 404、关 Marketing 带活动的保存抛 404;无人要求套件自己的代码被拒
③ 防错:A 静默但有界(数据保留、流程不派发、手册写明);B 响亮在错误的读者面前 —— 终端用户在运行期撞 404,AI 写的套件测试全绿上线才坏
④ 不扩散:A 不加门禁;B/C 各加一道引擎门禁且无维护者点名,B 再加 HotCRM 6 处改动
Prior rulings read: disabled package object in-process hook ctx.api engine served refused OBJECT_PACKAGE_DISABLED → 11 hits (nearest #25296, #25308: /actions 门拒绝停用包的动作,落在公开门;#7557 (A) 两类读者,钉在 package-disable-enforcement.test.ts;#24097/#24210 按名读取;none rules the in-process path); disabled package flows actions hooks still run in-process engine automation inert → 38 hits (nearest #27395 卸载后流程仍可触发、#20677 账本停用流程重启再武装,皆为自动化账本侧,none rules the engine read path); ADR-0016 §9.5, ADR-0141 open item 4; thread: 6523684730
Analysis-tier: fable
Generated by Claude Code
objectstack-fleet commentedon Oct 31, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Landing record · #28117 · PR #28308
domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-10-31T05:24Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28308-*'git merge-base --is-ancestor 7abe151b origin/maingit grep -c isDisableableAppModule origin/main -- packages/runtime/src/domains/packages.ts7abe151b. The changes, all under "disabled = hidden, refused on the data plane, data kept":type: 'module'package of an installed app.isWritablePackage, so authoring a module still answers 422. An app hook's in-processctx.apiaccess to a disabled module is still served; it is pinned and documented in the marketplace handbook.6538936519.Blocked-by: #28318.Release: claim
6538161407(session_0186vfjHHMsRXC9EUT9hTFMo, card threadsession_01FEkQ5otJRNvRGLG91cCJr7, branchclaude/project-thread-omgm6f) · into the decision box.pm:dispatched→needs-user-decision, assignee cleared. Under letter A the card closes with a ruling record. Under B or C a fresh claim and order follow.mainat7abe151bor later.Generated by Claude Code