Repository navigation
Commit 35afb15
fix(metadata-protocol): a packaged item's save answers the package door before the checks that judge its body, on every kernel topology (#22338)
Fixes #22220
Clause-②: no
## What changes
`saveMetaItem`'s package door (`refusePackagedBaseOverride`) is now
asked on every kernel topology, at the position it already had on an
environment kernel: after the code-only and organization-scope refusals,
before the item lock and before every check that reads the body or the
store. It used to sit behind `environmentId !== undefined`. A
host-config kernel (the CLI's assembler, the showcase's boot shape,
`OS_MODE=off`) met the same predicate only at
`SysMetadataRepository.assertAllowed`, the first statement of
`repo.put`, which is the method's last act. So on that kernel every
refusal in between answered first.
- `packages/metadata-protocol/src/protocol.ts`: the `environmentId`
wrapper around the door is removed. The `_lock` gate's guard
`packagedBaseRefusal(...) === null` (its hand-written deferral to the
door on host-config) always answered "no refusal" once the door runs
first on every kernel, so it is removed. The invariant comment on that
gate is extended, and the door's call site records why no acceptance set
moves. Two TSDoc paragraphs that said the door is environment-only are
corrected.
- New pins:
`packages/metadata-protocol/src/protocol.package-door-before-gates.test.ts`
(one table over both kernel topologies, `code` + `status` per row).
- Three downstream test files pinned the old host-config ordering and
are updated (below).
- `scripts/engine-double-contract.pinned.json`: the new pin file's
`findOne` double, recorded by `check-engine-double-contract.mjs
--write`.
- `.changeset/22220-package-door-before-gates.md`:
`@objectstack/metadata-protocol` patch.
## Door table, live (base `4e4111ca05` vs head `9e763ec0bc`, both built
and booted before the `main` merge)
Seeded admin, default composition, `OS_METADATA_WRITABLE` unset. Bodies:
**served** = the `GET` item; **gate-refused** = served plus one
autonumber field whose format names a missing field
(`autonumber-references-unknown-field`); **spec-refused** = served plus
an undeclared top-level key (`unrecognized_keys`). Every cell is `status
code`.
`examples/app-crm`, `PUT /api/v1/meta/object/crm_account` (packaged
under `com.example.crm`; `object` is `allowOrgOverride: false`).
Environment kernel = `pnpm dev:crm -- --fresh` (`env_local`).
Host-config kernel = the same stack under `OS_MODE=off` (the lightweight
assembler, `environmentId` undefined; confirmed by the repository's
sentence on the base row).
| body, mode | env kernel, base | env kernel, head | host-config, base |
host-config, head |
|:--|:--|:--|:--|:--|
| served, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE |
| served, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE |
| gate-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE |
**422 INVALID_METADATA** | 403 NOT_OVERRIDABLE |
| gate-refused, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE |
| spec-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE |
**422 INVALID_METADATA** | 403 NOT_OVERRIDABLE |
| spec-refused, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE |
**422 INVALID_METADATA** | 403 NOT_OVERRIDABLE |
| `permission/crm_sales_user`, spec-refused, publish | not measured |
403 NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE |
| `position/sales_rep`, spec-refused, publish | not measured | 403
NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE |
| control: env-local `zz_local_obj`, gate-refused, publish | 422
INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422
INVALID_METADATA |
| control: env-local `zz_local_obj`, spec-refused, publish | 422
INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422
INVALID_METADATA |
At head the host-config 403 carries the environment kernel's sentence:
the two `crm_account` gate-refused publish bodies compare byte-equal. At
base the host-config 403s carried the repository's sentence (`'object'
is not allowOrgOverride in the registry ...`), and a packaged
`permission`'s plain publish carried plugin-security's lock sentence.
The environment kernel's code path is unchanged by this diff, which is
why its two unmeasured base cells are left unmeasured rather than
inferred.
The card's own composition, `examples/app-showcase` (`pnpm dev --
--fresh`, host-config), `PUT /api/v1/meta/object/showcase_task`: base
answered 422 for gate-refused publish and for spec-refused publish and
draft, 403 for the rest; head answers 403 `NOT_OVERRIDABLE` for all six
rows, and the env-local controls answer 422 `INVALID_METADATA`.
## Door table, unit pins (both kernels; base = `protocol.ts` at the
merge base, head = this branch)
Measured through the real `saveMetaItem` over an engine double (the new
pin file's harness). "gate reached" = `assertRuntimeAuthoringRules` was
called.
| request | env base | env head | host-config base | host-config head |
|:--|:--|:--|:--|:--|
| object, served body, publish | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE, gate reached | 403
NOT_OVERRIDABLE |
| object, gate-refused, publish | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 422 INVALID_METADATA, gate reached | 403
NOT_OVERRIDABLE |
| object, gate-refused, draft | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE, gate reached | 403
NOT_OVERRIDABLE |
| object, spec-refused, publish | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE |
| object, spec-refused, draft | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE |
| object, `?package=` naming its read-only package, gate-refused,
publish | 403 ITEM_LOCKED | 403 ITEM_LOCKED | 422 INVALID_METADATA, gate
reached | 403 ITEM_LOCKED |
| object, fields dropped (destructive), publish | 403 NOT_OVERRIDABLE |
403 NOT_OVERRIDABLE | 409 DESTRUCTIVE_CHANGE | 403 NOT_OVERRIDABLE |
| position, spec-refused, publish | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE |
| permission, spec-refused, publish | 403 NOT_OVERRIDABLE | 403
NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE |
| control: env-local object, gate-refused, publish | 422, gate reached |
422, gate reached | 422, gate reached | 422, gate reached |
| control: env-local object, spec-refused, publish | 422
INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422
INVALID_METADATA |
| control: packaged view (`allowOrgOverride: true`), spec-refused | 422
INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422
INVALID_METADATA |
Registry-wide (a packaged `pkg_TYPE` with a spec-refused body, publish,
every type in `DEFAULT_METADATA_TYPE_REGISTRY`): at base the host-config
kernel answered differently from the environment kernel for 15 types
(`object` 409 `DESTRUCTIVE_CHANGE`; `hook`, `seed`, `mapping`, `page`,
`app`, `action`, `dataset`, `datasource`, `doc`, `book`, `permission`,
`position`, `tool`, `skill` 422 `INVALID_METADATA`). At head both
kernels give the same envelope for every type, and every type the door
governs (`allowOrgOverride: false`, `allowRuntimeCreate: true`) answers
403 `NOT_OVERRIDABLE`. The other 13 types answered alike on both kernels
at base and at head.
## The window the door now precedes (M2)
Refusals `saveMetaItem` could answer on a host-config kernel between the
door's position and `repo.put`, for a packaged `allowOrgOverride: false`
save:
- the ADR-0010 `_lock` gate, 403 `ITEM_LOCKED` (already deferred to the
door by hand; that guard is removed);
- the ADR-0029 D9.9 package mismatch, 422
`OBJECT_OVERLAY_PACKAGE_MISMATCH`;
- the destructive diff, 409 `DESTRUCTIVE_CHANGE` (measured above);
- the layered-envelope refusal, 422 `INVALID_METADATA`; the save-name
refusal, 400 `VALIDATION_ERROR`;
- the flow conversion conflict, 409 `FLOW_CONVERSION_CONFLICT`;
- the spec-conformance parse, 422 `INVALID_METADATA`, draft and publish
(measured);
- the stored-hook body refusal, 400 `VALIDATION_ERROR`;
- the runtime authoring gate, 422 `INVALID_METADATA`, publish only
(measured);
- the domain plugins' authoring gates: plugin-security's permission-set
lock (403 `NOT_OVERRIDABLE`, its own error class and sentence; measured
live) and its object posture gate R1 (403, wire `code`
`PERMISSION_DENIED`, `declaredCode` `owd_widening_forbidden`).
The view-container collision refusal also sits there but judges only
`view`, which allows overlays, so the door never precedes it. ADR-0070
D1 (`WRITABLE_PACKAGE_REQUIRED`) judges only `runtime-only` writes,
which the door never refuses.
## Mechanism assumptions, as measured
- **M1** reproduced at base on both compositions above. The
authoring-gate body was built from what `main` refuses
(`autonumber-references-unknown-field`); no pass-4 lift was needed.
- **M2** the spec parse also answers 422 ahead of the door on
host-config, in draft and publish mode; the full window is listed above.
- **M3** the predicate is `refusePackagedBaseOverride`'s own, already
topology-independent (`packagedBaseRefusal` asks it on every topology
for the `/automation` doors). It and `assertAllowed` read the same
registry-derived `allowOrgOverride` set, the same `OS_METADATA_WRITABLE`
hatch and the same `isWritablePackage` (`package-writability.ts`), and
both throw the same `readOnlyBaseOverrideError` for a named read-only
base. They differ only in the fallback sentence for a type with no
ADR-0126 regime row.
- **M4** read `0b997ea447`. The door's input is `isArtifactBacked`, the
same input the repository's intent uses, so stack-declared positions are
refused by the door on both kernels (measured live for
`position/sales_rep`). The `security`-domain types are `permission`,
`position` and `capability`; `capability` is code-only and answers the
code-only refusal ahead of the door, unchanged.
- **M5** held: a draft is still not judged by the authoring gate;
env-local bodies still answer 422 on both kernels; a packaged `view` and
a packaged object with the hatch open are still judged by the gates.
## What does not move (Clause-② measurement, on this head)
- **Accept sets.** The door refuses exactly when `artifactBacked &&
!isOverlayAllowed(type)`. `repo.put` runs `assertAllowed` with intent
`override-artifact` whenever `artifactBacked`, and that refuses on the
same predicate, on every topology. `saveMetaItem` has no success return
before `repo.put` (the one `return` in that window is inside a closure).
So a request the door refuses was refused before, and a request it
admits meets the same checks as before. The unit tables above show every
measured row refused at base and at head.
- **Built entry declarations.** `dist/index.d.ts` and `dist/index.d.cts`
of `@objectstack/metadata-protocol`, built from this head and from the
merge base's `protocol.ts` (`fe98cc63a4`): the only differences are the
two TSDoc paragraphs corrected above. No declaration line changes.
- **Wire.** For a packaged `allowOrgOverride: false` save on a
host-config kernel that one of the checks above refused, the answer is
now 403 `NOT_OVERRIDABLE` (or 403 `ITEM_LOCKED` for a named read-only
base) with the door's sentence, which is what an environment kernel
already answered.
## Downstream pins that encoded the old host-config order
Each relied on the host-config kernel skipping the door. Each now
reaches the check it tests the way an environment kernel always had to.
- `packages/objectql/src/protocol-destructive.test.ts`: saved a
destructive edit of a packaged object with no `environmentId` "to bypass
the overlay opt-in gate". It now opens `OS_METADATA_WRITABLE=object`,
the one route by which a packaged object's write reaches the destructive
diff. Expectations unchanged.
- `packages/rest/src/meta-object-owd-gate.test.ts`: the lint-before-R1
order case and the two R1 refusals drove a packaged-object overlay with
the hatch shut. They now open the hatch (the path R1's docblock names).
Expectations unchanged. Two comments that said the door was
environment-scoped are corrected.
-
`packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts`:
the hatch-closed case pinned the lock's error class on host-config. With
the hatch closed the door now answers first there, as on an environment
kernel, so the case asserts the same `NOT_OVERRIDABLE` / 403 envelope
and that the class is not the lock's. The hatch-open cases still pin the
lock's class.
## Verification
- Reverse verification: the new pin file run against the merge base's
`protocol.ts` (swapped on disk, blob-verified, restored by trap to the
HEAD blob): 23 failed / 26 passed of 49, every failure a host-config row
or a registry row; at head 49/49.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 222
files passed, 3 skipped; 28329 tests passed (at `9e763ec0bc`, before the
`main` merge, which touched only the seed loader in this package).
- At `81a42b1203`, after `git merge origin/main` and a rebuild: the new
pin file 49/49; `objectql` `protocol-destructive.test.ts` 7/7;
`plugin-security` `packaged-permission-set-lock-gate.test.ts` 7/7;
`rest` `meta-object-owd-gate.test.ts` 14/14; the five `qa/dogfood` files
that drive `saveMetaItem` 27/27. `typecheck` (with
`check:test-typecheck` where the package has it) green for
`metadata-protocol`, `objectql`, `plugin-security` and `rest`.
- Before the merge: `objectql` full `--project local` (383 files passed;
the 3 failures were `protocol-destructive.test.ts`, updated above),
`plugin-security` full (181 files passed; the 1 failure was the
lock-gate case updated above), the 35 `runtime` files and 24 `rest`
files that call `saveMetaItem` (all green except the 3
`meta-object-owd-gate` cases updated above).
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` derives 78
commands on `81a42b1203`. 70 ran on `32d94c2d00` (the merge commit; the
one later commit only adds the ledger row); the 8 that row adds, the
changeset gates and the ratchet families re-ran on `81a42b1203`. 77
exited 0; `--ran` reconciles 78 derived, 77 run, 1 unrun.
`check:engine-double-contract` first exited 1 for the missing ledger row
and exits 0 with it recorded. `check:type-check-debt` (a repository-wide
re-measure) hit a 400 s local timeout: **NOT MEASURED**, left to CI.
- Lint, narrowed: ESLint's own config matches 5 of the 7 changed paths
(the changeset and the JSON ledger answer "no matching configuration");
`--format json` reports 5 files, 0 errors, 0 warnings. This config
enables no type-aware linting, so the diff cannot move a verdict on an
untouched file.
## Serial notes
- PR #22319 edits `saveMetaItem`'s flow canonicalization and spec-parse
region; this diff stays out of those lines.
- PR #22323 edits `protocol.ts` around the drafts listing and
`sys-metadata-repository.ts`; no overlap with this diff.
## Acceptance notes
-
`packages/metadata-protocol/src/protocol.read-lock-flags-write-door.test.ts`'s
`hostConfigDoor` docblock still says `saveMetaItem` skips its package
door on host-config. The test measures `repo.put` directly and stays
correct; only that sentence is now stale. Not edited here (outside the
claimed file surface).
- `packaged-base-regime.ts` and `sys-metadata-repository.ts` describe
the protocol door as environment-scoped (incomplete now, not false). Not
edited here, for the same reason.
- ADR-0005 §"Whitelist enforcement" still says single-kernel deployments
keep "any type writable". The repository's `assertAllowed` has refused
these writes on those kernels since before this change; this diff moves
no acceptance set, so it reverses no ADR decision. The ADR text is stale
relative to shipped behaviour, not to this change.
- `deleteMetaItem` keeps its own `environmentId`-scoped removal door;
this card is about the save door only.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 41d0d40 commit 35afb15
7 files changed
Lines changed: 461 additions & 61 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- objectql/src
- plugins/plugin-security/src
- rest/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
Lines changed: 323 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 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
0 commit comments