Skip to content

fix: a disabled package's objects leave /auth/me/permissions, and actions that launch into it are no longer served - #28969

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ads4rl
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ads4rl

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Requested by Leehom · project thread

Fixes #28877

Clause-②: yes (narrowing)
Metadata-protocol: none

维护者速读

  • 改了什么:包被停用后,/auth/me/permissions 不再授予该包的对象(current_user.can() 随之答 false);另一个启用包里"启动进停用包"的动作(flow 目标只有停用包提供、或参数 objectOverride 指向停用包对象)不再出现在对象的动作列表和 /meta/action 里;绑在停用包对象上的动作也不再列在 /meta/action。
  • 为什么改: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 裁决 A 规定停用 = 隐藏 + 数据面拒绝。HotCRM 实测这两处仍按启用作答:按钮照画、点开报 "Flow not found" 或参数框报错。
  • 风险与回滚:只在有包被停用时生效,重新启用即恢复,无需重启。执行面(getMetaItemsForExecution)、代码层(getArtifactItem)和包导出不受影响。回滚 = revert 本 PR。
  • 先看:packages/objectql/src/registry.ts(isActionWithheldByDisabledPackage 与 attachBoundActions 的自有动作过滤)、packages/core/src/security/effective-object-permissions.ts(第 7 步)、packages/metadata-protocol/src/protocol.ts(action 列表过滤只在 effective served 面)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/bound-action-foreign-object.test.ts src/package-disable-enforcement.test.ts,预期 47 条全绿,其中 [#28877] 两组共 8 条。
  • 需要你做的:无,席位落地。

What changes

Row 1: the permission map. buildEffectiveObjectPermissions (@objectstack/core) gains an optional isObjectPackageDisabled accessor on its schema source and, as a new last step, leaves out every object it answers true for. The '*' entry is not an object and stays. registryObjectPackageDisabled(registry) is the one reading of that accessor: the engine registry's own isObjectPackageDisabled, the predicate the data plane asks before 404 OBJECT_PACKAGE_DISABLED. Both builders of the map hand it over, so /auth/me/permissions (plugin-hono-server) and ISecurityService.getEffectiveObjectPermissions (plugin-security, the map the server's own current_user.can() reads) stay byte-for-byte the same answer. An object the map does not mention answers can() false (@objectstack/formula's binding).

Row 2: actions that launch into a disabled package. #24400's rule lives in SchemaRegistry.boundServedActions: a disabled package's bound actions are not attached. It now asks isActionWithheldByDisabledPackage(action), which extends that rule: the action's own package is disabled (unchanged), OR its objectName object belongs to a disabled package (see Acceptance notes), OR a type: 'flow' target is a flow only disabled packages ship (isFlowOfDisabledPackage: at least one registered flow of that name, all of them in disabled packages), OR a param's objectOverride names an object of a disabled package (isObjectPackageDisabled). The same predicate is asked in three more places, and nowhere else:

  • attachBoundActions on the SERVED layer filters the object's own actions too. HotCRM's generate_quote and add_contact_to_campaign sit there, because defineStack() merges a same-stack bound action into its object. The code layer keeps what the package ships.
  • The protocol's effective served list for action (GET /meta/action) and the served by-name read (GET /meta/action/NAME, answered as a name with nothing behind it), through one helper isActionWithheldByDisabledPackage(registry, item) beside isItemOfDisabledPackage. The execution list and the per-package list (the package export) do not ask it.

Cache: a disable or enable already drops every fold (#24400). A flow registered or unregistered while some package is disabled, a package uninstall that removes flows, and the uninstall of a disabled package now drop every fold too, since each can change a verdict.

Row 3 (the param dialog's wording) is not changed here: with the action withheld, the dialog is not reached.

Why

#28117 ruling A: a disabled package is hidden and refused on the data plane. HotCRM measured on 18.2.0 (hotcrm#2383 comment 6567537276) that with a module disabled, /auth/me/permissions still granted its objects (so every action gated on current_user.can() stayed drawn), and an app action launching the module's flow was still served and failed with "Flow 'quote_generation' not found".

Tests

  • packages/objectql/src/bound-action-foreign-object.test.ts, new block [#28877] (5 cases) on the spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400 harness (real ObjectQL + ObjectStackProtocolImplementation): a MODULE package with an object and a flow, a LAUNCHER package with an inline flow action, a bound flow action, an objectOverride action and three controls (an enabled flow, a field param on its own object, a URL naming the module's object in its text). Control before the disable; after it, the served object (getMetaItem object and registry.getObject), getMetaItems action and the by-name action read withhold exactly the two launching actions plus the inline one; re-enable restores them with no restart; a flow an enabled package also ships (registered after the disable) keeps the action; the execution list and the code layer keep everything.
  • packages/objectql/src/package-disable-enforcement.test.ts, new block [#28877] (3 cases) on the real SchemaRegistry harness: a plain wildcard, an explicit grant and a super-user seed all reach the map while enabled; disabled, the object leaves the map and can() answers false for read, create, edit and delete, the other package is untouched, '*' stays, and the data plane refuses the same object OBJECT_PACKAGE_DISABLED; re-enabled, the grant is back.
  • packages/core/src/security/effective-object-permissions.test.ts (4 cases): the omission over each entry source, '*' kept, no accessor or a throwing one leaves the map whole (warned), and registryObjectPackageDisabled asks the registry's own method.
  • plugin-hono-server current-user-endpoints-effective-objects.test.ts (1 case) and plugin-security get-effective-object-permissions.test.ts (1 case, and the shared endpointObjects reference now hands the accessor too): each door leaves out a disabled object, byte-equal to the core function; the security case evaluates current_user.can('deal', 'read') through ExpressionEngine and gets false.

Acceptance notes

  • Admin and member readers. Row 1 is measured for a super-user (ops_admin, objects reached by the seed) and a member (sales, an explicit grant) in the security pin, and for the plain-wildcard and explicit-grant shapes in the objectql pin. Row 2 reads no caller: the withholding is in the served metadata, the same for every reader.
  • Bounded in-place fix: an action bound to a disabled package's object. The order says "withheld wherever spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400 withholds an action bound to a disabled package's object". Measured on the first head (de04ea7, whose predicate had no objectName arm, so this case read as on main): with the module disabled, an enabled package's url action bound by objectName to the module's object was still in getMetaItems({ type: 'action' }) and served by name, while getMetaItem of that object threw OBJECT_PACKAGE_DISABLED (probe output listed= true byName= true object= OBJECT_PACKAGE_DISABLED). spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400 hid it only by hiding its object. Same defect class, one mechanical arm on the same predicate, same files and gate families, so it is fixed here: the predicate's objectName arm, pinned by bound_to_member in the [#28877] block (ablating the arm reds only the "disabled" case, 46/47).
  • The dispatch door is unchanged. POST /actions/OBJECT/ACTION reads the action through the execution face, which this PR does not filter. What a withheld action answers there depends on where it is declared. One that keeps a top-level copy (a bound action registered as a standalone action item, as HotCRM's generate_quote is) still resolves and answers the launch's own refusal: 404 "Flow … not found" for a disabled flow. One declared only inside its object's actions (like inline_enroll in the test) is no longer in the served object it is resolved from, so the door answers the ADR-0110 D3 "no declaration" 404 (reading from the contract review, comment 6569625622; not re-measured on this head). Either way the console no longer draws the button, so the path is not reached from it.
  • The fields slot of /auth/me/permissions is not pruned: its keys are OBJECT.FIELD entries of authored sets, and the order names the objects map only.
  • Row 3. The param dialog's text is not reworded: once row 2 withholds the action, the dialog is not reached.

Verification

All readings below are on head 934a10f5 unless a block says otherwise. Head 0a0e0022 changes only the changeset (five packages minor to patch, Metadata-protocol: none); check-empty-changeset, check-changeset-no-major and check-adr-0087-registration (and its --self-test) exit 0 on it. Every ablation went through scripts/ablation-replace.mjs (or a git checkout BASE leg with a trap restore and HEAD-blob hash check), each mutated package was rebuilt, and ablation-dist-preflight.mjs proved the marker left or reached dist/ before the run was read. Every restore ended with git diff HEAD empty and git status --porcelain clean.

Red first on main: row 2 (the three row-2 sources at BASE b15b634, metadata-protocol rebuilt)

objectql bound-action-foreign-object.test.ts + package-disable-enforcement.test.ts: 2 failed / 45 passed. Red: "disabled: the flow-target and objectOverride actions leave the served object and GET /meta/action" and "a flow an enabled package also ships keeps the action…" (its first assertion reads main serving the action). Green on main as they should be: the before-disable control, re-enable, and execution list / code layer. Every row-1 file green (core 41/41, hono 6/6, security 65/65).

Red first: row 1 (the omission step ablated in core, core rebuilt)

Exactly the row-1 cases red, and every row-2 case green: core 3 failed / 38 passed (the three omission cases), objectql 1 failed / 46 (the [#28877] "disabled" grants case), hono 1 failed / 5, security 1 failed / 64. On main the doors hand no accessor, so this is main's answer.

Ablations, each half reds only its own cases
Mutation Red Green
A3 protocol filter only (two metadata-protocol files at BASE, registry kept) "disabled" case, on the getMetaItems assertion (served object assertion passed) 28 others
A4 flow-target arm "disabled", "a flow an enabled package also ships" 27 others
A5 objectOverride arm "disabled" 28 others
A6 served-object own-actions filter (kept = base) "disabled" (inline action) 28 others
A7 flow-registration invalidation "a flow an enabled package also ships" (stale fold) 28 others
A8 objectName arm (on 934a10f) "disabled" 46 others
Package suites, typecheck, full dogfood

On de04ea75 (the second commit adds one predicate arm and its pin, re-run in its own files on 934a10f: objectql 47/47):

  • pnpm --filter @objectstack/core test: 129 files, 2896 tests passed
  • pnpm --filter @objectstack/objectql test: 524 files, 10044 passed
  • pnpm --filter @objectstack/metadata-protocol test: 297 passed, 4 skipped; 22093 passed, 26 skipped
  • pnpm --filter @objectstack/plugin-hono-server test: 37 files, 467 passed
  • pnpm --filter @objectstack/plugin-security test: 238 files, 5127 passed, 45 skipped
  • pnpm --filter PKG run typecheck for each of the five: exit 0 each (objectql's includes check:test-typecheck)

Full dogfood on 934a10f5 ((narrowing) arm), closure built, under the verify lock: VERDICT command-exit 0, Test Files 382 passed, 3 skipped (385); Tests 3156 passed, 28 skipped (3184). No failure.

Gates: the derived union on 934a10f

node scripts/pm/dispatch-gates.mjs --commands derived 71 commands; all ran on this head. 70 exit 0, among them pnpm check:type-check-debt (re-run once the verify lock was free: "0 raw tsc error(s) total, none above its recorded number") and pnpm check:i18n. One is a build prerequisite, NOT MEASURED locally and left to CI:

  • pnpm check:dual-build-cjs-loads exit 3: "Run pnpm build first. This is NOT a pass: nothing was measured" (needs every package's dist/).

Overlap re-read before opening: open PRs searched for effective-object-permissions, isObjectPackageDisabled, attachBoundActions, "disabled package", "me/permissions"; the two hits (#28903, #28889) touch none of this diff's files.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (no spec change)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (CI owns the repo-wide lint run)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the lane's required Chinese summary).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5nncLaiWjVh2cS7U17

…ns launching into it are withheld

The effective object-permission map (/auth/me/permissions and
ISecurityService.getEffectiveObjectPermissions) leaves out an object whose
owning package is disabled, read through the registry's own
isObjectPackageDisabled. The registry's served object fold and the
protocol's served action list and by-name read withhold an action whose
flow target only disabled packages ship, or whose param objectOverride
object belongs to a disabled package.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nncLaiWjVh2cS7U17
… action list

The registry's withholding predicate also answers for an action whose
objectName object belongs to a disabled package: that object's own read
is refused, so the action is no longer listed beside it in the served
action list and by-name read.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nncLaiWjVh2cS7U17
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/plugin-hono-server, @objectstack/plugin-security, touching 23 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/security/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ff88105061012c8b14f7ec937d7b3113e404e922.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/security/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 61 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ff88105061012c8b14f7ec937d7b3113e404e922 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 74fc7b4d305b168d7c329958d009a8022d42172f — the merge of head 0a0e00229cd391adb51908655bf967c0008bef4b into base ff88105061012c8b14f7ec937d7b3113e404e922, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 74fc7b4d305b168d7c329958d009a8022d42172f && git checkout 74fc7b4d305b168d7c329958d009a8022d42172f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ff88105061012c8b14f7ec937d7b3113e404e922 0a0e00229cd391adb51908655bf967c0008bef4b && git checkout -B drift-repro ff88105061012c8b14f7ec937d7b3113e404e922 && git merge --no-ff 0a0e00229cd391adb51908655bf967c0008bef4b

node scripts/docs-audit/affected-docs.mjs --json ff88105061012c8b14f7ec937d7b3113e404e922

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ff88105061012c8b14f7ec937d7b3113e404e922 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 934a10f59b985b8de6035b99e2434b0b7ea21457
Local-runs: none
Scope: packages/core/src/security/effective-object-permissions.ts, packages/core/src/security/effective-object-permissions.test.ts, packages/core/src/security/index.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-meta-item-face.ts, packages/objectql/src/registry.ts, packages/objectql/src/bound-action-foreign-object.test.ts, packages/objectql/src/package-disable-enforcement.test.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints-effective-objects.test.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts + packages/runtime/src/action-execution.ts (resolveRouteActionDeclaration), packages/runtime/src/domains/actions.ts (the dispatch door), packages/runtime/src/domains/packages.ts (the enable door's gate), packages/objectql/src/engine.ts (getSchema, the current_user.can() resolver seam), scripts/check-changeset-no-major.mjs and scripts/check-adr-0087-registration.mjs (ruling A on #25105, the level rule), .changeset/28877-disabled-package-grants-and-launch-actions.md (the declaration under review, read against the rule)

Reviewed at 2026-11-02T05:13Z against merge-base b15b634c16845712490ae43f889516e813c9390a (two commits: de04ea75, 934a10f5). Face: Clause-②: yes (narrowing) on the PR body and the changeset ⇒ the narrowing face; record-recognisers --paths finds no schema or governed path, so the floor is every path of the diff outside the prose faces (all twelve above). Author objectstack-fleet[bot].

① Derived judgments

Row 1 — /auth/me/permissions leaves out a disabled package's objects. RIGHT.

  • One predicate, no second spelling. registryObjectPackageDisabled(registry) calls registry.isObjectPackageDisabled(name) on the registry itself. The data plane's refusal (assertObjectRegistered → isObjectOfDisabledPackage in protocol.ts) calls the same method. SchemaRegistry.isObjectPackageDisabled resolves the name through resolveObjectKey and asks the OWNER's package (#7557), so the map leaves out exactly the objects /data/<object> refuses 404 OBJECT_PACKAGE_DISABLED. The objectql pin asserts both halves on one harness (assertObjectRegistered throws the code; the map has no entry).
  • Byte-identical doors. The only two production builders of the map are plugin-hono-server (/auth/me/permissions) and plugin-security (ISecurityService.getEffectiveObjectPermissions); both hand isObjectPackageDisabled: registryObjectPackageDisabled(ql?.registry). The engine's current_user.can() resolver seam is registered by the security plugin with that same method (registerEffectiveObjectPermissionsResolver), so the console, the server's visibleWhen/formula/defaultValue evaluation and the write wall read one map. The hono and security pins assert JSON.stringify equality with the core function; the security pin evaluates current_user.can('deal','read') through ExpressionEngine and gets false for an admin (super-user seed) and a member (explicit grant). The hono route's catch answers the closed shape and needs no accessor.
  • can() false: the omission runs LAST, over the explicit grant, the super-user seed and the wildcard's coverage; the '*' entry is kept (it is not an object, and objectPermissionGrants reads the object's own entry). Pinned per source in core and for read/create/edit/delete in objectql.
  • Re-enable without restart: core caches nothing; the security plugin memoises set resolution, not the map; the registry's enablePackage flips the record in place. Pinned in objectql, hono and security.
  • No caller left out. git grep buildEffectiveObjectPermissions on the head: the two doors above and the tests; nothing builds the map without the accessor. Failure posture: a registry without the method disables nothing, and a throwing predicate warns and leaves the map whole (the same guard as the seed and the apiOperations annotation). That is fail-open to main's answer, display-only, since the data plane still refuses the object; it matches isObjectOfDisabledPackage's optional-call posture and is acceptable.
  • Not pruned, by the order's wording: the fields slot still carries OBJECT.FIELD entries of a disabled object. can() does not read it; the dev records it as a carrier-seat finding. Noted, not a defect of this PR.

Row 2 — an action launching into a disabled package is withheld where #24400 withholds. RIGHT, with one acceptance-note correction (should-fix 1).

  • The seam is spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400's. boundServedActions replaced isPackageDisabled(_packageId) with isActionWithheldByDisabledPackage(action), whose first arm is that same test, so spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400's rule is a strict subset. The three new arms: objectName object of a disabled package (see Premise correction), type: 'flow' target that only disabled packages ship (isFlowOfDisabledPackage), and a param objectOverride naming an object of a disabled package (isObjectPackageDisabled, the row-1 predicate). hasDisabledPackage() short-circuits the walk when nothing is disabled, so an enabled installation pays nothing and serves exactly what it served.
  • Three served faces, one predicate: (a) the served object fold — attachBoundActions on the 'served' layer filters the object's OWN actions (where defineStack() leaves a same-stack bound action) and attaches only boundServedActions; registry.getObject and the protocol's getMetaItem({type:'object'}) both end there; (b) the protocol's effective served list — readFlattenedMetaItems filters only when request.type === 'action' && audience === 'served' && servedOverride (layer: 'effective'), which is what GET /meta/action calls; (c) the served by-name read — readMetaItemFace with face === 'served' answers a withheld action as a name with nothing behind it, as isItemOfDisabledPackage does. All three are pinned in one block on the real ObjectQL + protocol harness.
  • Unchanged, and rightly: getMetaItemsForExecution / getMetaItemForExecution (audience 'execution'), getMetaItemsPerPackage (layer: 'per-package', servedOverride false — the package export), and the code layer (attachBoundActions(..., 'code'), getArtifactItem). The execution plane keeps the declaration so the ADR-0066 D4 gate, the activation ledger and the dispatch trichotomy still read it; the package export keeps what the package ships. Pinned ("the execution list and the code layer keep what each package ships").
  • "Flow target only served by a disabled package": isFlowOfDisabledPackage answers true only when at least one flow of that name is registered AND every one is owned by a disabled package; a twin flow from an enabled package, a runtime-authored row with no _packageId, or a name the registry holds no flow for are not disabled. Right: the automation engine binds from the list, which omits only a disabled package's items, so a twin keeps the launch alive. Pinned including the twin registered AFTER the disable (which exercises the new registerItem('flow') invalidation).
  • Cache invalidation is complete for the new verdict inputs: a flow registered or unregistered while any package is disabled, a package uninstall that removes flow/ items, and the uninstall of a disabled package each invalidateAll(); disable/enable already did (spec(object-extension): the extension refusal says defineStack() attaches a top-level action with objectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400). A7 reddens exactly the twin-flow case.
  • A withheld own-action still blocks an attached twin of its name (present is built from base, not kept): deliberate, so nothing stands in for a withheld action.

Premise correction (the objectName arm). IN SCOPE as a bounded in-place fix, and declared. The order says "withheld wherever #24400 withholds an action bound to a disabled package's object" on GET /api/v1/meta/action. The dev measured on the first head that #24400 withheld it only by hiding the object: the enabled package's action bound by objectName to the module's object was still listed and served by name (listed= true byName= true object= OBJECT_PACKAGE_DISABLED). The arm is one line on the same predicate, in the same files and gate families, pinned by bound_to_member in the [#28877] block, ablated A8 (reddens only that case, 46/47), and declared in the changeset's FROM (third bullet) and TO (objectql bullet) and in the PR body's Acceptance notes. Not scope creep: without it row 2's "wherever #24400 withholds" would be false on /meta/action. The seat records the premise correction on the card at hand-back.

Row 3. Not touched; answered by row 2 (the dialog is not reached once the action is withheld). The seat answers row 3 on the card.

Caller classes. Admin and member are both measured for row 1 (security pin: ops_admin seed, sales grant; objectql pin: wildcard and explicit grant). Row 2 reads no caller: the withholding is in the served metadata. Nothing that legitimately needs a disabled package loses access: the Packages page reads GET /api/v1/packages (the package type is never filtered) and PATCH /packages/:id/enable is gated on the manage_metadata system permission, not the object map; /meta/object already omitted the disabled objects before this PR. A system context gets the same omission, which is consistent: assertObjectRegistered refuses every caller. The dispatch door POST /actions/OBJECT/ACTION is unchanged in code; see should-fix 1 for its answer.

Out of scope held. No change to navigation, the disable door or the data-plane refusal.

Evidence. Red first: row 2 at BASE for the three row-2 files reddens exactly the "disabled" and twin-flow cases (2/47), row-1 files green; row 1 with the omission ablated reddens exactly the omission cases in all four packages, row-2 cases green. Ablations A3–A8 each redden only their own cases (A3 protocol filter: the list assertion only; A4 flow arm: "disabled" + twin; A5 override arm; A6 own-actions filter: the inline action; A7 flow-registration invalidation: the twin; A8 objectName arm). Controls: before-disable, re-enable, twin flow, execution list, code layer, '*' kept, the other package untouched. Full package suites green on de04ea75, the second commit re-run in its own files on 934a10f5 (47/47); typecheck exit 0 on all five. Full dogfood battery on 934a10f5 (narrowing arm): 382 files / 3156 tests passed, no failure. Gates: 71 derived, 70 exit 0, check:dual-build-cjs-loads NOT MEASURED locally (needs every dist/), left to CI's Build Core. Overlap: #28903 (app-shell PackagesPage, UI) and #28889 (closed, UI permissions provider) touch none of this diff's files.

CI on the head at review time: no red. Completed success: Check Changeset, Governed Surface Queue Guard, Dogfood Regression Gate, every Lint lane, Type Check · source gates and · debt ledger, Live E2E UI, PR-body guards, Check PR Size, Flag docs. Still in progress: Build Core (carries dual-build-cjs-loads), Test Core 1–8, Type Check · workspace and · consumer gates, Build & E2E UI, Test UI 1–16, Type Check UI. The landing precheck ② is the seat's read of the final check-runs.

② Semver level

Declared: Clause-②: yes (narrowing) · Metadata-protocol: narrowed · minor on core, objectql, metadata-protocol, plugin-hono-server, plugin-security · BREAKING banner with FROM → TO · <!-- adr-0087: not-required (no-migration-prescription) … -->. PR body and changeset agree with each other, and the level follows the declared kind, so the gates are green.

Judged:

  • Clause-②: yes (narrowing) — RIGHT. The published surface widens (one optional member on EffectiveObjectPermissionsSchemaSource, registryObjectPackageDisabled, isActionWithheldByDisabledPackage on the registry and the protocol module) and two served answers narrow while a package is disabled. yes + the (narrowing) arm is the one legal spelling for widen+narrow.
  • Metadata-protocol: narrowed — WRONG. The kind is "the change to what a metadata author can write": narrowed is a key removed or tombstoned, a value refused, a requirement tightened. Nothing an author writes is refused or tightened here: every object, flow, action, type: 'flow' target and objectOverride is accepted exactly as before, and the changeset's own ADR-0087 marker says so in terms ("no spec key, authorable spelling, export or stored shape is removed, renamed or re-shaped"). What narrows is two served answers under a runtime package state. That is a non-protocol narrowing, and ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105 (check-changeset-no-major.mjs, its ruling block and self-test) says: "A non-protocol widening (REST) or narrowing (a driver, a posture) ships patch and keeps its BREAKING banner and its ADR-0087 disposition unchanged." The repo's precedent is exactly this shape: eleven changesets with Clause-②: yes (narrowing) + Metadata-protocol: none at patch (the orgless-boot refusal, the CBP master boot refusal, the plugin-auth load refusal, the datasource-refused line in objectql, …), each banner saying "it changes what boots/serves, not what a metadata author can write, so it ships as patch".
  • Consequence of the mis-declaration: narrowed is a breaking carrier of its own (signal 5 in the ADR-0087 gate) and the release-notes classifier files a narrowed bullet with a not-required disposition under "需手动处理 — action required: narrowed, with no ADR-0087 conversion", telling every reader of five packages' release notes that an authored key needs hand migration. None does. The five-package minor footprint follows the same error: under none every introduced entry is patch, and the level gate refuses a minor beside none, so the two lines and the five levels move together.
  • adr-0087: not-required (no-migration-prescription) — RIGHT, and still required after the fix: the (narrowing) arm and the BREAKING banner each declare breaking, so a disposition is owed, and this category is the honest one (no migration prescription anywhere in the body; the gate validated it, exit 0 locally and Check Changeset green).
  • Five packages, not one: right. Each of the five publishes a changed runtime (core: the function; objectql: the predicate and the fold; metadata-protocol: the list and by-name filters; the two plugins: the accessor handed over). After the fix, five patch entries.

Must-fix M1 (changeset + PR body, no code): in .changeset/28877-disabled-package-grants-and-launch-actions.md and the PR body, Metadata-protocol: none; all five frontmatter entries patch; the banner line reads "shipped as patch" (a served-answer narrowing, not what an author writes). Keep Clause-②: yes (narrowing), the BREAKING banner, FROM → TO and the adr-0087: not-required (no-migration-prescription) marker unchanged.

③ Boundary flags

Implemented-by: claude/project-thread-ads4rl
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: FAIL — one must-fix (M1, the Metadata-protocol: kind and the five levels; prose faces only, no code change). Rows 1 and 2 are correct against ruling A on #28117 and #24400's seam; re-review on the corrected head is a declaration read.


Generated by Claude Code

…tocol none

No authored value is refused, so the narrowing is not a metadata-protocol
one: every package takes patch and the BREAKING banner and its ADR-0087
disposition stay.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nncLaiWjVh2cS7U17
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0a0e00229cd391adb51908655bf967c0008bef4b
Local-runs: none
Scope: packages/core/src/security/effective-object-permissions.ts, packages/core/src/security/effective-object-permissions.test.ts, packages/core/src/security/index.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-meta-item-face.ts, packages/objectql/src/registry.ts, packages/objectql/src/bound-action-foreign-object.test.ts, packages/objectql/src/package-disable-enforcement.test.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints.ts, packages/plugins/plugin-hono-server/src/current-user-endpoints-effective-objects.test.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts + packages/runtime/src/action-execution.ts (resolveRouteActionDeclaration), packages/runtime/src/domains/actions.ts (the dispatch door), packages/spec/src/stack.zod.ts (mergeActionsIntoObjects, copies and keeps the top-level action), scripts/check-changeset-no-major.mjs (ruling A on #25105), .changeset/28877-disabled-package-grants-and-launch-actions.md (the corrected declaration)

Round 2, reviewed at 2026-11-02T05:23Z. Re-pin of the record on 934a10f5 (comment 6569625622, FAIL on M1) onto the new head. Merge-base unchanged (b15b634c). git diff 934a10f5..0a0e0022: one file, the changeset, +7/−7, nothing else — the code tree, the twelve floor paths and every test are byte-identical to the reviewed head, so ① of the first record stands unchanged and is not re-derived here. Author objectstack-fleet[bot].

① Derived judgments

  • Rows 1 and 2, the premise correction, caller classes, evidence and overlap: as recorded on 934a10f5, unchanged (same tree). RIGHT.
  • New PR body, checked for accuracy. Declaration lines read Clause-②: yes (narrowing) / Metadata-protocol: none, matching the changeset. No stale narrowed or minor wording remains anywhere in the body. The dispatch-door Acceptance bullet now separates the two cases correctly: an action with a top-level copy (a bound action registered as a standalone action item) still resolves through the execution by-name read and answers the launch's own 404 "Flow … not found"; an action declared only inside its object's actions is no longer in the served fold ql.getSchema reads, so the door answers the ADR-0110 D3 "no declaration" 404. Both facts hold on the code: resolveRouteActionDeclaration reads ql.getSchema = registry.getObject (served) first, then the execution by-name read. The HotCRM example is right: generate_quote is a top-level action with objectName: 'crm_opportunity' (src/sales/actions/opportunity.actions.ts in the hotcrm checkout) and defineStack()'s merge only copies it into the object, keeping the top-level item. The bullet says the inline case is read from the review and not re-measured on this head; honest, and S1 is closed.
  • The Verification block says what the new head changes (changeset only) and that the three changeset gates exit 0 on it locally; CI confirms the same (below), which is the reading this record relies on.

② Semver level

  • M1 closed. Changeset frontmatter: core, objectql, metadata-protocol, plugin-hono-server, plugin-security all patch. Body: Clause-②: yes (narrowing), Metadata-protocol: none, the BREAKING banner now reads "shipped as patch: a non-protocol narrowing takes patch and keeps this banner and its ADR-0087 disposition (no authored value is refused)", FROM → TO unchanged, <!-- adr-0087: not-required (no-migration-prescription) … --> unchanged. That is exactly ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105 (check-changeset-no-major.mjs: "A non-protocol widening (REST) or narrowing (a driver, a posture) ships patch and keeps its BREAKING banner and its ADR-0087 disposition unchanged") and the repo's precedent for served-answer and boot-refusal narrowings.
  • Level and kind agree: none ⇒ every introduced entry patch; the (narrowing) arm and the banner still declare breaking, so the ADR-0087 disposition is owed and not-required (no-migration-prescription) remains the honest one (no migration prescription in the body).
  • Changeset gates on the head, read from CI check-runs. Two Check Changeset runs exist on 0a0e0022: the first (started before the PR body was patched) is red — its annotation quotes Metadata-protocol: narrowed read from the PR body against the five patch levels, i.e. the body/changeset disagreement that existed for a few minutes, not a defect of the head. The re-run after the body edit (job 125109296368) is completed :: success: check-empty-changeset (added 1, none modified), check-adr-0087-registration (one declared-breaking changeset, not-required (no-migration-prescription), notice-level only) and check-changeset-no-major all pass. The newest run governs. Governed Surface Queue Guard, PR-body guards, every Lint lane, Dogfood Regression Gate, Type Check · source gates and · debt ledger: success.

③ Boundary flags

Implemented-by: claude/project-thread-ads4rl
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS — M1 and S1 closed on 0a0e0022; declaration, levels and disposition agree with ruling A; the code tree is the one reviewed on 934a10f5.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T05:24Z · head 0a0e0022 · claimed by seat 2 on #28877

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
  2. Contract review at CONTRACT_REVIEW_TIER:
    • Round 1 on 934a10f5 (6569625622) failed on M1 only, the changeset level and declaration.
    • Round 2 on this head 0a0e0022 (6569759873) is a PASS. M1 is closed, the code tree is identical to round 1's, and S1 (the dispatch-door bullet in the body) is closed.
  3. Gates:
    • The red Check Changeset run (125107869678) read the body before the body edit. Its re-run on this head (125109296368) is green.
    • The remaining lanes were still running at the time of this ACCEPT, with no red. Auto-merge waits for them, and the merge queue re-runs CI against current main.
  4. Serial: none.
  5. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit d0df873 Nov 2, 2026
67 of 68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ads4rl branch November 2, 2026 06:02
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/l tests tooling

Projects

None yet

1 participant