Repository navigation
fix: a disabled package's objects leave /auth/me/permissions, and actions that launch into it are no longer served - #28969
Conversation
…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
📓 Docs Drift CheckThis PR changes 5 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 61 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 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
|
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Reviewed at 2026-11-02T05:13Z against merge-base ① Derived judgmentsRow 1 —
Row 2 — an action launching into a disabled package is withheld where #24400 withholds. RIGHT, with one acceptance-note correction (should-fix 1).
Premise correction (the 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: 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, 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 ② Semver levelDeclared: Judged:
Must-fix M1 (changeset + PR body, no code): in ③ Boundary flags
Implemented-by: VERDICT: FAIL — one must-fix (M1, the 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
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Round 2, reviewed at 2026-11-02T05:23Z. Re-pin of the record on ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS — M1 and S1 closed on Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
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。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 optionalisObjectPackageDisabledaccessor on its schema source and, as a new last step, leaves out every object it answerstruefor. The'*'entry is not an object and stays.registryObjectPackageDisabled(registry)is the one reading of that accessor: the engine registry's ownisObjectPackageDisabled, the predicate the data plane asks before404 OBJECT_PACKAGE_DISABLED. Both builders of the map hand it over, so/auth/me/permissions(plugin-hono-server) andISecurityService.getEffectiveObjectPermissions(plugin-security, the map the server's owncurrent_user.can()reads) stay byte-for-byte the same answer. An object the map does not mention answerscan()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 asksisActionWithheldByDisabledPackage(action), which extends that rule: the action's own package is disabled (unchanged), OR itsobjectNameobject belongs to a disabled package (see Acceptance notes), OR atype: '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'sobjectOverridenames an object of a disabled package (isObjectPackageDisabled). The same predicate is asked in three more places, and nowhere else:attachBoundActionson the SERVED layer filters the object's ownactionstoo. HotCRM'sgenerate_quoteandadd_contact_to_campaignsit there, becausedefineStack()merges a same-stack bound action into its object. The code layer keeps what the package ships.action(GET /meta/action) and the served by-name read (GET /meta/action/NAME, answered as a name with nothing behind it), through one helperisActionWithheldByDisabledPackage(registry, item)besideisItemOfDisabledPackage. 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/permissionsstill granted its objects (so every action gated oncurrent_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 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 harness (realObjectQL+ObjectStackProtocolImplementation): a MODULE package with an object and a flow, a LAUNCHER package with an inline flow action, a bound flow action, anobjectOverrideaction 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 (getMetaItemobject andregistry.getObject),getMetaItemsaction 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 realSchemaRegistryharness: a plain wildcard, an explicit grant and a super-user seed all reach the map while enabled; disabled, the object leaves the map andcan()answers false for read, create, edit and delete, the other package is untouched,'*'stays, and the data plane refuses the same objectOBJECT_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), andregistryObjectPackageDisabledasks the registry's own method.plugin-hono-servercurrent-user-endpoints-effective-objects.test.ts(1 case) andplugin-securityget-effective-object-permissions.test.ts(1 case, and the sharedendpointObjectsreference now hands the accessor too): each door leaves out a disabled object, byte-equal to the core function; the security case evaluatescurrent_user.can('deal', 'read')throughExpressionEngineand getsfalse.Acceptance notes
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.defineStack()attaches a top-level action withobjectName: '<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 noobjectNamearm, so this case read as onmain): with the module disabled, an enabled package'surlaction bound byobjectNameto the module's object was still ingetMetaItems({ type: 'action' })and served by name, whilegetMetaItemof that object threwOBJECT_PACKAGE_DISABLED(probe outputlisted= true byName= true object= OBJECT_PACKAGE_DISABLED). 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 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'sobjectNamearm, pinned bybound_to_memberin the[#28877]block (ablating the arm reds only the "disabled" case, 46/47).POST /actions/OBJECT/ACTIONreads 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 standaloneactionitem, as HotCRM'sgenerate_quoteis) still resolves and answers the launch's own refusal:404"Flow … not found" for a disabled flow. One declared only inside its object'sactions(likeinline_enrollin 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.fieldsslot of/auth/me/permissionsis not pruned: its keys areOBJECT.FIELDentries of authored sets, and the order names theobjectsmap only.Verification
All readings below are on head
934a10f5unless a block says otherwise. Head0a0e0022changes only the changeset (five packagesminortopatch,Metadata-protocol: none);check-empty-changeset,check-changeset-no-majorandcheck-adr-0087-registration(and its--self-test) exit 0 on it. Every ablation went throughscripts/ablation-replace.mjs(or agit checkout BASEleg with atraprestore and HEAD-blob hash check), each mutated package was rebuilt, andablation-dist-preflight.mjsproved the marker left or reacheddist/before the run was read. Every restore ended withgit diff HEADempty andgit status --porcelainclean.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
getMetaItemsassertion (served object assertion passed)objectOverridearmkept = base)objectNamearm (on 934a10f)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 passedpnpm --filter @objectstack/objectql test: 524 files, 10044 passedpnpm --filter @objectstack/metadata-protocol test: 297 passed, 4 skipped; 22093 passed, 26 skippedpnpm --filter @objectstack/plugin-hono-server test: 37 files, 467 passedpnpm --filter @objectstack/plugin-security test: 238 files, 5127 passed, 45 skippedpnpm --filter PKG run typecheckfor each of the five: exit 0 each (objectql's includescheck: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 --commandsderived 71 commands; all ran on this head. 70 exit 0, among thempnpm check:type-check-debt(re-run once the verify lock was free: "0 raw tsc error(s) total, none above its recorded number") andpnpm check:i18n. One is a build prerequisite, NOT MEASURED locally and left to CI:pnpm check:dual-build-cjs-loadsexit 3: "Runpnpm buildfirst. This is NOT a pass: nothing was measured" (needs every package'sdist/).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
packages/specholds no runtime logic. (no spec change)pnpm lintpasses. (CI owns the repo-wide lint run)main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5nncLaiWjVh2cS7U17