Repository navigation
fix(plugin-security): the position refusal reads no retired catalog table and names the catalog type it judged against; the unimported per-organization catalog module is deleted - #24365
Conversation
… the retired catalog objects The suite booted `sys_position`, `sys_permission_set` and `sys_position_permission_set` from the legacy testkit so it could seed rows the production composition can no longer hold: none of the three is registered on main, and neither the refusal's verdict nor the authorization resolver reads them. Booted as production boots, every case still passes against the unchanged source; the two cases that asserted the id-to-name hint now read the generic remedy, because the hint's `sys_position` read answers OBJECT_NOT_FOUND and is swallowed. Claude-Session: https://claude.ai/code/session_01Y5583bs2RDd53MH6kM1ca3 Co-authored-by: Claude <noreply@anthropic.com>
…able and names the catalog type it judged against `idSpellingHints` looked a refused value up as a `sys_position` record id to offer that row's name. `sys_position` is unregistered on main, so the read answered OBJECT_NOT_FOUND, the catch swallowed it, and the hint was never offered: it is deleted, with the record-id read context and the hint arm of the message. The envelope's `constraint.target` named the same retired table; it now names the security catalog type the value is judged against, `position`, as the sibling permission-set grant refusal names `permission`. The suite pins both: the two `constraint` assertions read `position`, and a record id refused from inside an organization reads no `sys_position`. Claude-Session: https://claude.ai/code/session_01Y5583bs2RDd53MH6kM1ca3 Co-authored-by: Claude <noreply@anthropic.com>
…, which nothing imports `per-organization-catalog.ts` was left holding one export, `rowMatchesDeclaration`, after the retired seeders that called it were deleted; nothing imports it. Its suite kept one resolver case (case 6), which never touched the module: it pinned `resolveUserAuthzGrants` against `sys_position` and junction rows booted from the legacy testkit. Core's own resolver suites pin the same three assertions against the resolver's source: a member outside the binding organization collects nothing an organization bound, a member inside it collects nothing either, and the definition's set reaches both. They also pin that the resolver reads none of the retired catalog tables at all. The receiver-detach gate's must-not-fire control cited the deleted file as the measured home of the `logger?.error?.(message)` spelling; it now cites a live site of the same spelling. Changeset for the package. Claude-Session: https://claude.ai/code/session_01Y5583bs2RDd53MH6kM1ca3 Co-authored-by: Claude <noreply@anthropic.com>
…tired-catalog-residue
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 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 ade857c54a33f4a77ea72aa2cc10fdb6d4c1d537 && git checkout ade857c54a33f4a77ea72aa2cc10fdb6d4c1d537
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4526a70557f34fa8717a4802dc8dddae3237fd7 2fb7b3621e98857b90bf86678fce97ea9d8a7b0f && git checkout -B drift-repro c4526a70557f34fa8717a4802dc8dddae3237fd7 && git merge --no-ff 2fb7b3621e98857b90bf86678fce97ea9d8a7b0f
node scripts/docs-audit/affected-docs.mjs --json c4526a70557f34fa8717a4802dc8dddae3237fd7
|
席位核对 · ACCEPT at head
|
Fixes #15196
Part of #15204
Clause-②: no
What this does
This is the last item of #15196 and stage 6c of #15204, in one PR. Both parts remove residue the retired security catalog left behind, and both do it by deletion. That follows the cutover batch rule (6094179271): only delete, never fix the retired modules.
position-catalog-refusal.ts. The id-to-name hint (idSpellingHints) readsys_position, which is not registered onmain. The hint is deleted, together with its record-id read context and the hint form of the message. The refusal envelope'sconstraint.targetalso named that retired table. It now namesposition, the security catalog type the value is judged against.position-catalog-refusal.test.ts. The suite no longer bootssys_position,sys_permission_setandsys_position_permission_setfrom the legacy testkit. It now boots the way production does.per-organization-catalog.tsand its test are deleted. The module had 28 lines, one export and no importers.scripts/check-logger-receiver-detach.mjs. The gate's must-not-fire control had a provenance comment naming the deleted file. It now names a live file that uses the same spelling..changeset/15196-retired-catalog-residue.md.Landing point: the file surface of claims 6251403239 and 6251408540. Nothing outside it changed.
Measurements
These were taken on
origin/maindd72a047, in this branch's worktree.M1: the hint never fires in production. Confirmed.
sys_position.git grep "name: 'sys_position'"finds 0 hits outside tests (control:name: 'sys_user_position'finds its object file).git grep '\bSysPosition\b'finds 0 hits outside tests and testkits.ObjectQLengine overSqlDriverwith nosys_positionregistered.find('sys_position', …)throwsOBJECT_NOT_FOUND(404). The control,findon a registered object, answers[]. The hint'scatchswallowed that throw, so it never returned a hint.35a9404c: with the suite moved off the legacy testkit and the source still unchanged, all 25 cases pass. The two record-id cases already read the generic remedy, so the hint was absent before any source change.securityCatalogReaderOfreads the registry, then the metadata service, and never a row. Apart from thesys_user_positionpre-image, the hint was the module's only row read.M2: readers of this refusal's
constraint.target. None outside its own suite.constraint.targetorconstraint?.target. The only two hits are this suite's own pins. The refusal builds its message itself rather than throughrenderValidationMessage, whose six callers are other refusals. So no{{target}}template ever renders this value./home/user/objectuiat43efd07):git grepover*.tsand*.tsxforconstraint.target,constraint?.targetand.constraintreaders finds one hit. It is a TypeScript ASTtypeParameter.constraintin a guard test, unrelated to field errors. The field-error consumers readcodeandmessageonly:plugin-grid/src/ImportWizard.tsxswitches oncode, andAssignedUsersSectionshowsmessage. No renderer and no i18n key reads'sys_position'from a refusal.target: 'position', withtargetField: 'name'unchanged.positionis the security catalog type the refusal resolves the value against (POSITION_CATALOG_TYPE). Reasons:sys_user_permission_set.permission_set(grant-permission-set-name.ts) already sendstarget: 'permission', pinned in its suite, so both catalog-reference refusals now use the same form.The engine's lookup refusal puts an object name in
targetbecause a lookup points at an object. This column points at a catalog type.FieldErrorSchema.constraintstays an open record.fields[].constraint.targetgoes fromsys_positiontoposition. I found no reader of it in either repo, and the changeset states the new value.POSITION_CATALOG_OBJECTandidSpellingHintswere exported from an internal module only. Neither was in the package'sindex.ts.M3:
per-organization-catalog.tshas no importer. Confirmed.rowMatchesDeclarationhas one hit: its own definition.per-organization-catalogappears in no import specifier. The remaining hits are prose: ADR-0131, theplugin-sharingcomment covered under M4, and the gate's control comment.import … namesWithoutCatalogRowline in the refusal suite.M4: stale pointers.
scripts/check-logger-receiver-detach.mjs:503named the deleted file as the place thelogger?.error?.(message)spelling was measured. It now namespackages/plugins/plugin-auth/src/member-role-canonical.ts, which uses that spelling at:481and is on no retirement list.packages/plugins/plugin-sharing/src/bootstrap-declared-sharing-rules.ts:47is left as it is: C3 stage 9 (C3 stage 9 (#15204): declared sharing rules read from the registry, bootstrapDeclaredSharingRules retires, and the ceremony deletes the declared-rule mirrors; the verified marker follows (director ordering ruling) #23952, in flight) retires that file, and the claim excludes it.Test deletion: case 6 of
per-organization-catalog.test.tsCase 6 never imported the module it was filed under. It pinned
resolveUserAuthzGrantsfrom@objectstack/core, reached through core'sdist/, againstsys_positionand junction rows booted from the legacy testkit. Core's own resolver suites pin each of its assertions, importing the resolver from its source:packages/core/src/security/resolve-authz-context.test.ts› §6a › "a member removed from alpha: the definition's set and nothing alpha bound — with no tenant and in beta"everyonedefinition names reaches both members['global_everyone_default']; alsoresolve-authz-context.grant-set-by-name.test.ts› "theeveryonedefinition's set resolves; a junction row binding another set confers nothing"resolve-authz-context.grant-set-by-name.test.ts› "the junction, the id bridge and the catalog rows are never read". The resolver reads none ofsys_position,sys_position_permission_setorsys_permission_set.Run here:
pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/resolve-authz-context.test.ts src/security/resolve-authz-context.grant-set-by-name.test.ts. Result: 2 files and 120 tests passed, including all four named cases.Case 6 used a real driver to pin how the engine's scope and the driver's predicate combined during a
sys_positionsweep. The resolver no longer reads that table (the last row above), so there is nothing of that kind left to pin.No case of
position-catalog-refusal.test.tswas deleted: it had 25 cases before and has 25 after. The hint assertions changed along with the behaviour they pinned, and now expect the generic remedy. The walled case that used a name only one organization's row carried now compares two names the catalog does not hold. The walled record-id case gained one assertion: it reads nosys_position.Reverse verification
This was a one-shot check at
7427cac5, run throughscripts/ablation-replace.mjs. For each leg the anchor hit exactly once and the file's blob hash changed. After each leg the restore was proven: the blob equals HEAD andgit diff HEADis empty. The suite imports the subject by relative source path (./security-plugin.js), so there is nodist/step.constraint.targetset back to'sys_position'. Result: 2 failed, 23 passed. The failures are exactly the twoconstraintpins (- "target": "position"/+ "target": "sys_position").sys_positionrecord-id read planted back before the throw. Result: 1 failed, 24 passed. The failure is the walled record-id case:expected [ 'sys_position', 'sys_position' ] to not include 'sys_position'.Tests and gates (at
2fb7b362)pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2(the package'stestscript isvitest run): 187 files passed; 3736 tests passed and 45 skipped. Lock VERDICTcommand-exit 0.pnpm --filter @objectstack/plugin-security typecheck: lock VERDICTcommand-exit 0.src/position-catalog-refusal.test.tsandsrc/per-organization-catalog.test.tspassed 26 of 26. After it,src/position-catalog-refusal.test.tspasses 25 of 25.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 88 families. I ran all 88 at2fb7b362after a fullturbo run buildand recorded each exit code before any pipe: all 88 exited 0.--ranreconciliation:✓ dispatch-gates --ran: 88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED.pnpm check:pm-dispatch-gatesran detached, as its header asks, and exited 0.--ranalso flagged thatorigin/mainmoved 6 commits during the run (toc4526a70). None of those commits touchesplugin-securityor any file in this diff. CI runs the merge ref.Acceptance notes
packages/plugins/plugin-security/src/security-plugin.ts:4317still says the refusal fires when a position "names nosys_positioncatalog row". In fact the verdict reads the security catalog. That file is outside this claim's file surface, so I did not edit it. Carrier: none.content/docs/permissions/system-context.mdxstill mentions "a name only asys_positionrow carries". That is still true of rows left in an existing database, so I left it.position-catalog-refusal.tsmodule doc describe the verdict against leftoversys_positionrows. They are still accurate, so I left them.Generated by Claude Code