Repository navigation
fix(objectql): register stack-declared positions under their package so the save door refuses overrides - #22262
Conversation
The metadata save door refuses a save over a package-declared item of a type with no overlay channel, and decides "a package ships this" from the engine SchemaRegistry entry the package registered. METADATA_ARRAY_KEYS carried permissions and capabilities but still carried the retired `roles` spelling instead of `positions`, so a stack-declared position had no such entry and a save over it took the runtime-create tier. Adds `positions` to the provenance seam, drops the stale waiver row in check-stack-collection-maps, re-measures the seeder declaration-copy pin on a real artifact boot, and pins every security-domain allowOrgOverride:false type at the door on both topologies. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…sition-allow-org-override
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 17 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 20ac611edde4da6e30354f252ec6307af2402a2e && git checkout 20ac611edde4da6e30354f252ec6307af2402a2e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a 7ed88a608124b912c391119d1b2d0e111ee7d5cf && git checkout -B drift-repro 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a && git merge --no-ff 7ed88a608124b912c391119d1b2d0e111ee7d5cf
node scripts/docs-audit/affected-docs.mjs --json 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a
|
Contract reviewServed-tier: Inputs: card #22203 (body; comments 6054396188 triage, 6055084143 claim, 6057851358 os-dev-report), PR #22262 (body, its 7-file list, the net diff against ① Derived judgments
② Semver level
③ Boundary flagsThe deliberate correction —
|
Landing note:
|
Fixes #22203
Clause-②: no
What changes
ObjectQL.registerApp()and the nested-plugin seam now register a stack'spositionscollection into the engine SchemaRegistry under the owning package. They stamp the same ADR-0010 provenance thatpermissionsandcapabilitiesalready get (METADATA_ARRAY_KEYS,packages/objectql/src/engine.ts). The metadata save door's existing type-level packaged-base check then coverspositionthe same way it covers the otherallowOrgOverride: falsetypes.Landing: the producer side, in
packages/objectql. The save door inpackages/metadata-protocolwas correct and was given the wrong input, so its code is unchanged. There is nopackages/specedit; the flag was already declared.Measured: why the type-level check answered for one type and not the other
allowOrgOverridein two places:refusePackagedBaseOverrideinsidesaveMetaItem(environment-scoped kernel) andSysMetadataRepository.assertAllowedunder theoverride-artifactintent (host-config kernel).isArtifactBacked→lookupArtifactItem→SchemaRegistry.getArtifactItem. That lookup only finds entries a package registered with_packageId.registerMetadataCollectionsoverMETADATA_ARRAY_KEYS. That list hadpermissionsandcapabilities. For positions it still had the retiredrolesspelling: ADR-0090 D3's rename reachedARTIFACT_FIELD_TO_TYPE(packages/metadata/src/plugin.ts) and never reached this list.check:stack-collection-mapsrecorded the absence as a waiver. The waiver's reason pointed at the metadata service's registry, not the SchemaRegistry.isArtifactBacked('position', NAME)answered false. The save took theruntime-onlyintent, andallowRuntimeCreate: trueaccepted it.plugin-security's packaged permission-set lock answers first. That lock isregisterPackagedPermissionSetLockGate, an authoring gate registered forpermissiononly. Under it, the type-level door also refuses a package-declared permission set. The pin below shows this with no security plugin composed. The lock stays as it is, a stricter type-specific layer on top of the type-level door.Population of the pin, read from the registry: every
domain: 'security'row withallowOrgOverride: false, which today ispermission,positionandcapability.Door table
Showcase (
pnpm dev -- --fresh), host-config kernel, seeded admin. The same requests were sent both times:positionsentry ablated. objectql was rebuilt, and the preflight proved the change reacheddist/.7ed88a6081(mergedorigin/mainf4bed58341).The same position, permission, capability and control answers were also measured before the merge, at base
7b926f7600and headf7d8d0e172.NOT_OVERRIDABLE?package=)WRITABLE_PACKAGE_REQUIREDITEM_LOCKEDplugin-security)NOT_OVERRIDABLENOT_OVERRIDABLE(unchanged)?package=NOT_OVERRIDABLENOT_OVERRIDABLENOT_OVERRIDABLENOT_OVERRIDABLEallowOrgOverride: true)/meta/positionBoot diagnostics: 4 warnings on both boots, the same four. The seeded
sys_positionrows carry the declared labels and descriptions.Pins
New:
packages/objectql/src/engine-security-catalog-package-door.test.ts. It uses a realObjectQL, a realObjectStackProtocolImplementationand the population above, read fromDEFAULT_METADATA_TYPE_REGISTRY. For each type it checks:{ code: 'NOT_OVERRIDABLE', status: 403 }and stores nothing, on both topologies.It also checks that the by-name read still serves the package's position after the refusal. Controls:
email_template(allowOrgOverride: true) still saves over its packaged item, on both topologies.Re-measured:
packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Its positions case asserted the old absence on an artifact that declared no position. The probe artifact now declares one, and the case asserts the registry holds it under the artifact's package beside the six built-ins. This is a realcreateStandaloneStackboot withSecurityPlugin.Widened:
packages/objectql/src/engine-nested-plugin-collections.test.ts.positionsjoins the property candidates: both seams register it identically.Gate:
scripts/check-stack-collection-maps.mjs. The stalemissing: ['positions']waiver onMETADATA_ARRAY_KEYSis removed. The gate fails on a stale waiver, and its reason was wrong about which registry it meant.Reverse verification (ablation)
The run was committed first, then mutated through
scripts/ablation-replace.mjs. The mutation removed'positions'fromMETADATA_ARRAY_KEYS: anchor count 1 → 0, blob8465f68a50a1→c4414cf91029. objectql was then rebuilt, andscripts/ablation-dist-preflight.mjs @objectstack/objectql '"positions"' --absent --source-marker="'positions'"exited 0.Predicted before running: 4 red in the new pin (seam, both topology refusals, by-name read) and 1 red in the runtime pin, everything else green. Measured exactly that:
Tests 4 failed | 47 passed (51);Tests 1 failed | 12 passed (13).Restore leg: blob == HEAD,
git diff HEADempty. objectql was rebuilt, and the preflight in default mode found the marker present in 4 built files with a clean tree. Both suites were green again: 51/51 and 13/13.The new pin was also run at base
7b926f7600before any fix existed. Exactly the 4 position cases were red, and the by-name read returned the environment fork.Local verification
All at head
7ed88a6081unless noted.pnpm --filter @objectstack/objectql test: 381 files / 7536 tests passed, at5188b2ad45. The merge brought noobjectql,metadata-protocolorcorechange.pnpm --filter @objectstack/objectql typecheckandpnpm --filter @objectstack/runtime typecheck: OK, test layers included.standalone-stack,standalone-stack-seeder-declaration-copy,standalone-stack-security-registrarandapp-plugin-artifact-forward-conversion: 48/48;artifact-collections: 8/8;pnpm --filter @objectstack/plugin-security test: 3739 passed, 45 skipped;security-catalog-showcase,showcase-declarative-rbac-seeding,position-address-readersandrls-runner: 53/53. These resolvedist/, rebuilt at this head.node scripts/pm/dispatch-gates.mjs --commandsgives 86 families, all run at7ed88a6081and reconciled with--ran.check-empty-changesetexits 1, by design. See the next section.pnpm check:pm-dispatch-gates: exit 0, with all 1976 self-test cases passing. The 900 s per-gate cap in the batch runner killed it first, so it was re-run on its own with its exit code captured..ts/.mjsfiles, at7ed88a6081.--format jsonreports 5 files linted, 0 errors, 0 warnings, none ignored.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any file it does not touch.A pending release note this PR corrects: please confirm
.changeset/15196-core-security-catalog-read.md(pending,@objectstack/core) says the engine registry carries "no stack-declared position, and the metadata service carries the stack-declared positions". This PR makes that sentence false, so the sentence is rewritten in place to say the engine registry also carries stack-declared positions. No other text changed.check-empty-changesetstays red on it until a person confirms the correction. That is the gate's own route for a deliberate correction, and the file is not restored. If the seat prefers, the correction can be split into its own docs-only PR instead.For #22220 (serial, same region)
This PR does not touch
saveMetaItem,refusePackagedBaseOverride,packagedBaseRefusalorSysMetadataRepository.assertAllowed. The override check keeps its position and order, where its inputs come from, and its emitters. What changes is the value of one input:isArtifactBacked(type, name)now answers true for a stack-declared position. So the check now fires for positions where it already fired for permission sets: insaveMetaItembehindenvironmentId, and in the repository'sassertAllowedon a host-config kernel. #22220's reorder of the package door against the authoring gate will seepositionbehave likepermissionon the type-level path, without theplugin-securityauthoring lock, which is registered forpermissiononly.Acceptance notes (noted, not filed)
Older artifacts that spell the collection
roles: still open, measured, not fixed here. An artifact whose protocol floor predates ADR-0090 D3 can declare positions under the older collection key. Such a position still takes the runtime-create tier at the save door:registerMetadataCollections), so no SchemaRegistry entry exists for it.Measured on a scratch
createStandaloneStackboot. Control: the canonical key on the same boot is refused 403NOT_OVERRIDABLE. The scratch file was deleted. This belongs to the [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy ofpermissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 / Route ownership for the five artifact security collections — MEASURED: the two registrars' copies differ in TYPE on a key a consumer can read today, not just on some future retired key #12892 raw-copy divergence family and is handed to the seat in the report.Stale comment in
packages/metadata-protocol/src/protocol.ts. TheisNestedArtifactFieldTSDoc cites the org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743 census, which listspositionamong types that "genuinely ship no artifacts at all". That is no longer true. Carrier: metadata-protocol: on a host-config kernel the object save door runs the authoring gate before the package door, so a packaged object's publish save can answer 422 INVALID_METADATA where an environment kernel answers 403 NOT_OVERRIDABLE (#8184's sibling) #22220, the next PR in that file.Stale text in
packages/core/src/security/security-catalog.ts. The module doc's measurement table is dated to3d9188502eand the constructionTypeErrorrationale says the engine registry holds no stack-declared position. The read order and the result set are unchanged: the dogfood catalog suite stays green, and its header anticipates this exact move. Carrier: feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 (ADR-0131 C2).Refusal wording for
position. On a host-config kernel the refusal forpositionuses the repository's generic sentence, which mentionsOS_METADATA_WRITABLE.positionhas no ADR-0126 regime row (permissionhas one: clone). No new wording is added here.Not in scope, per triage. The cold-boot ordering boundary described on the card.
Generated by Claude Code