Repository navigation
Commit 0b997ea
fix(objectql): register stack-declared positions under their package so the save door refuses overrides (#22262)
Fixes #22203
Clause-②: no
## What changes
`ObjectQL.registerApp()` and the nested-plugin seam now register a
stack's `positions` collection into the engine SchemaRegistry under the
owning package. They stamp the same ADR-0010 provenance that
`permissions` and `capabilities` already get (`METADATA_ARRAY_KEYS`,
`packages/objectql/src/engine.ts`). The metadata save door's existing
type-level packaged-base check then covers `position` the same way it
covers the other `allowOrgOverride: false` types.
Landing: the producer side, in `packages/objectql`. The save door in
`packages/metadata-protocol` was correct and was given the wrong input,
so its code is unchanged. There is no `packages/spec` edit; the flag was
already declared.
## Measured: why the type-level check answered for one type and not the
other
- **The check is already type-level.** The packaged-base check reads the
registry's `allowOrgOverride` in two places:
`refusePackagedBaseOverride` inside `saveMetaItem` (environment-scoped
kernel) and `SysMetadataRepository.assertAllowed` under the
`override-artifact` intent (host-config kernel).
- **Its input comes from the engine registry.** Both decide "a code
package ships this item" through `isArtifactBacked` →
`lookupArtifactItem` → `SchemaRegistry.getArtifactItem`. That lookup
only finds entries a package registered with `_packageId`.
- **The input was missing for positions.** The only seam that puts stack
collections into that registry under their package is
`registerMetadataCollections` over `METADATA_ARRAY_KEYS`. That list had
`permissions` and `capabilities`. For positions it still had the retired
`roles` spelling: ADR-0090 D3's rename reached `ARTIFACT_FIELD_TO_TYPE`
(`packages/metadata/src/plugin.ts`) and never reached this list.
`check:stack-collection-maps` recorded the absence as a waiver. The
waiver's reason pointed at the metadata service's registry, not the
SchemaRegistry.
- **Result.** For every stack-declared position,
`isArtifactBacked('position', NAME)` answered false. The save took the
`runtime-only` intent, and `allowRuntimeCreate: true` accepted it.
- **Where the permission-set 403 comes from.** On the showcase,
`plugin-security`'s packaged permission-set lock answers first. That
lock is `registerPackagedPermissionSetLockGate`, an authoring gate
registered for `permission` only. 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 with `allowOrgOverride: false`, which today is
`permission`, `position` and `capability`.
## Door table
Showcase (`pnpm dev -- --fresh`), host-config kernel, seeded admin. The
same requests were sent both times:
- **without fix:** this branch with the one added `positions` entry
ablated. objectql was rebuilt, and the preflight proved the change
reached `dist/`.
- **with fix:** head `7ed88a6081` (merged `origin/main` `f4bed58341`).
The same position, permission, capability and control answers were also
measured before the merge, at base `7b926f7600` and head `f7d8d0e172`.
| item | request | without fix | with fix |
|---|---|---|---|
| package-declared position | PUT | 200, saved | 403 `NOT_OVERRIDABLE` |
| package-declared position | PUT naming the package (`?package=`) | 422
`WRITABLE_PACKAGE_REQUIRED` | 403 `ITEM_LOCKED` |
| package-declared position | GET by name after the PUT | serves the
environment row | serves the package's position |
| built-in position (`plugin-security`) | PUT | 403 `NOT_OVERRIDABLE` |
403 `NOT_OVERRIDABLE` (unchanged) |
| package-declared permission set | PUT, with or without `?package=` |
403 `NOT_OVERRIDABLE` | 403 `NOT_OVERRIDABLE` |
| package-declared capability | PUT | 403 `NOT_OVERRIDABLE` | 403
`NOT_OVERRIDABLE` |
| position no package declares | PUT | 200 | 200 |
| package-declared dashboard (`allowOrgOverride: true`) | PUT | 200
overlay | 200 overlay |
| position list | GET `/meta/position` | 16 names | 16 names, no
duplicates |
Boot diagnostics: 4 warnings on both boots, the same four. The seeded
`sys_position` rows carry the declared labels and descriptions.
## Pins
- **New:
`packages/objectql/src/engine-security-catalog-package-door.test.ts`.**
It uses a real `ObjectQL`, a real `ObjectStackProtocolImplementation`
and the population above, read from `DEFAULT_METADATA_TYPE_REGISTRY`.
For each type it checks:
- the provenance seam registers the stack-declared item under its
package;
- a save over the package-declared item is refused with envelope `{
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:
- a position no package declares still saves, on both topologies;
- `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 real `createStandaloneStack` boot with
`SecurityPlugin`.
- **Widened:
`packages/objectql/src/engine-nested-plugin-collections.test.ts`.**
`positions` joins the property candidates: both seams register it
identically.
- **Gate: `scripts/check-stack-collection-maps.mjs`.** The stale
`missing: ['positions']` waiver on `METADATA_ARRAY_KEYS` is 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'` from
`METADATA_ARRAY_KEYS`: anchor count 1 → 0, blob `8465f68a50a1` →
`c4414cf91029`. objectql was then rebuilt, and
`scripts/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:
- objectql: `Tests 4 failed | 47 passed (51)`;
- runtime: `Tests 1 failed | 12 passed (13)`.
Restore leg: blob == HEAD, `git diff HEAD` empty. 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 `7b926f7600` before any fix existed.
Exactly the 4 position cases were red, and the by-name read returned the
environment fork.
## Local verification
All at head `7ed88a6081` unless noted.
- `pnpm --filter @objectstack/objectql test`: 381 files / 7536 tests
passed, at `5188b2ad45`. The merge brought no `objectql`,
`metadata-protocol` or `core` change.
- `pnpm --filter @objectstack/objectql typecheck` and `pnpm --filter
@objectstack/runtime typecheck`: OK, test layers included.
- Post-merge, targeted runs:
- objectql pin, seam and capability-provenance suites: 58/58;
- runtime `standalone-stack`,
`standalone-stack-seeder-declaration-copy`,
`standalone-stack-security-registrar` and
`app-plugin-artifact-forward-conversion`: 48/48;
- verify `artifact-collections`: 8/8;
- `pnpm --filter @objectstack/plugin-security test`: 3739 passed, 45
skipped;
- dogfood `security-catalog-showcase`,
`showcase-declarative-rbac-seeding`, `position-address-readers` and
`rls-runner`: 53/53. These resolve `dist/`, rebuilt at this head.
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands` gives
86 families, all run at `7ed88a6081` and reconciled with `--ran`.
- 84 exit 0.
- `check-empty-changeset` exits 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.
- ESLint, narrowed:
- **What was checked:** the 5 changed `.ts`/`.mjs` files, at
`7ed88a6081`.
- **Result:** `--format json` reports 5 files linted, 0 errors, 0
warnings, none ignored.
- **Why the narrowing excludes nothing:** `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`), so this diff cannot
change the verdict on any file it does not touch.
- Integration tier, full lint and the rest of the farm: declared to CI.
## 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-changeset` stays 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`,
`packagedBaseRefusal` or `SysMetadataRepository.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: in `saveMetaItem` behind `environmentId`, and in
the repository's `assertAllowed` on a host-config kernel. #22220's
reorder of the package door against the authoring gate will see
`position` behave like `permission` on the type-level path, without the
`plugin-security` authoring lock, which is registered for `permission`
only.
## 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:
- the artifact door converts the key, but only for the metadata service
copy;
- the engine registers the raw manifest bytes
(`registerMetadataCollections`), so no SchemaRegistry entry exists for
it.
Measured on a scratch `createStandaloneStack` boot. Control: the
canonical key on the same boot is refused 403 `NOT_OVERRIDABLE`. The
scratch file was deleted. This belongs to the #14491 / #12892 raw-copy
divergence family and is handed to the seat in the report.
- **Stale comment in `packages/metadata-protocol/src/protocol.ts`.** The
`isNestedArtifactField` TSDoc cites the #7743 census, which lists
`position` among types that "genuinely ship no artifacts at all". That
is no longer true. Carrier: #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 to `3d9188502e` and the
construction `TypeError` rationale 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: #15196 (ADR-0131 C2).
- **Refusal wording for `position`.** On a host-config kernel the
refusal for `position` uses the repository's generic sentence, which
mentions `OS_METADATA_WRITABLE`. `position` has no ADR-0126 regime row
(`permission` has 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](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c8bb3c8 commit 0b997ea
7 files changed
Lines changed: 326 additions & 18 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
188 | 188 | | |
189 | 189 | | |
190 | 190 | | |
191 | | - | |
| 191 | + | |
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
| |||
Lines changed: 276 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
0 commit comments