Repository navigation
Commit e030d43
feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) (#22365)
Fixes #22307
Clause-②: no
Executes the maintainer's ruling letter A on #22307 (ruling record
6063176077): the restart path refuses too. After `sys_metadata`
hydration and before `kernel:ready`, the engine checks every
package-held permission set and position name against the environment
catalog, and a name the environment already holds fails the boot with
the 422 `NAMESPACE_CONFLICT` envelope the package door uses, naming both
holders. A cold boot, a hot install and an artifact boot now answer
alike (Q4 = A, ruling record 6050490870).
The ADR-0048 addendum N.3 amendment is Tier H and rides its own draft
PR, from branch `claude/issue-22307-adr-0048-n3-amendment`. This PR
carries no `docs/adr/**` file.
## What changed
- **`packages/objectql/src/plugin.ts`.** `ObjectQLPlugin.start()` calls
a new private `refuseEnvironmentHeldSecurityCatalogNames()` right after
the hydration block (`restoreMetadataFromDb`, or the project-kernel skip
line) and before Phase 3's schema sync. Any conflict throws
`SecurityCatalogNameConflictError` with `door: 'cold-boot'`, which fails
`start()` and with it the boot. It runs whether or not the kernel
hydrated.
- **`packages/objectql/src/registry.ts`.**
- A private `SchemaRegistry.environmentHeldSecurityCatalogConflicts()`
returns every package-held position and permission-set name that also
has a bare-slot item. Built-in names are skipped. Results are sorted by
type, then name.
- A private `securityCatalogPackageHolders()` reads the package half of
the holder reading: composite slots and install claims, never the bare
slot.
- A module-level `findEnvironmentHeldSecurityCatalogNames(registry)` is
the plugin's handle on that reading. It is not re-exported from
`index.ts` or `core.ts`, so the public surface does not grow.
- `SecurityCatalogNameConflictError` takes an optional `{ door:
'cold-boot' }`, which changes only the message: which package declares
each name, and a remedy stated for a restart. `code`, `status`,
`httpStatus` and `conflicts[]` are unchanged.
- **`packages/objectql/src/security-catalog-namespace.ts`.**
`ENVIRONMENT_HELD_SECURITY_CATALOG_TYPES` (`position`, `permission`: the
two types the metadata-type registry declares `allowRuntimeCreate:
true`), and a module-doc section, "The cold boot".
- **`.changeset/22307-cold-boot-catalog-refusal.md`** (new).
`'@objectstack/objectql': major`, the BREAKING banner, the ADR-0087
marker `not-required (no-migration-prescription)`, the upgrade shape and
the remedy.
- **`.changeset/22135-security-catalog-one-holder.md`** (pending, not
yet released). See Acceptance notes, "A pending release note this PR
corrects".
-
**`scripts/adr-anchors/packages__objectql__src__security-catalog-namespace.ts.json`.**
The invariant gains the cold-boot half.
No new error code, no `packages/spec` change.
## Where each refusal sits (for the merge with #22331, which landed
first)
`main` was merged at e3ae92a, after #22331 landed. The merge was
clean, and the order in `ObjectQLPlugin.start()` on this head is:
1. #22331's `installDeploymentPlatformGlobalObjects(ctx)`, the first
statement of `start()`.
2. `restoreMetadataFromDb(ctx)`: `sys_metadata` hydration.
3. **This PR's `refuseEnvironmentHeldSecurityCatalogNames()`**: right
after the hydration `if`/`else` and before Phase 3's
`installRegisteredSchemas`. It runs before any plugin that depends on
the engine starts, and before `kernel:ready`.
4. #22331's `assertDeploymentPlatformGlobalObjectsUnchanged(ctx)`, at
the top of the `kernel:ready` hook.
The two changes share no hunk. This PR's new method sits directly after
`restoreMetadataFromDb`'s method body, and its import line comes after
the `picklist-resolution` import block.
## Mechanism assumptions, measured
- **M1, the admission today.** Reproduced through `bootStack` on one
database file, on the untouched base 28bff18. Boot 1 saved a
permission set and a position through `PUT /api/v1/meta/permission/NAME`
and `PUT /api/v1/meta/position/NAME`. Both answered `200`; a new
position name needs no `OS_METADATA_WRITABLE`. Boot 2, cold, added a
package declaring both: it booted, with two `[Registry] Collision`
warnings, and the by-name read answered the environment's definitions.
Boot 3 hot-installed the same package: `422 NAMESPACE_CONFLICT`, both
names held by `environment`.
- **M2, where the check sits.** As above. Boot shapes:
- standalone `os serve` / `os dev` / `bootStack`: `environmentId` unset,
hydration runs, the check runs (measured, dogfood);
- the artifact boot (`createStandaloneStack`): `environmentId:
'env_local'` with `hydrateMetadataFromDb: true`, hydration runs, the
check runs (measured, runtime pin);
- a project kernel with `environmentId` and no `hydrateMetadataFromDb`:
hydration is skipped, and the check runs over whatever reached the bare
slot, normally nothing (code reading);
- a host with no `protocol` service, or one without `loadMetaFromDb`:
nothing hydrates, and the check runs with nothing to judge (code
reading).
`loadMetadataFromService` at the top of `start()` syncs `object`,
`view`, `app`, `flow` and `hook` only, so no other boot-time path writes
these two types into the bare slot.
- **M3, the holder reading. Partly falsified, route changed by the
ruling's intent.** At a cold boot the hydrated environment row is NOT an
unstamped bare-slot item. Hydration runs after the package registered,
and the protocol's artifact-protection merge grafts the package's
envelope onto the stored row. Measured on base: the bare slot
`probe22307_set` carries `_packageId: com.probe.addon22307` and
`_provenance: package`, so #22197's stamp-based reading answers "the
package itself" and finds no second holder. The check therefore reads
every bare-slot item as the environment's, whatever stamp it wears: only
a registration with no package writes the bare slot. A package holds a
name through a composite slot or a claim, never through the bare slot.
The envelope class, holder kinds and claims are #22197's.
- **M4, built-ins.** Built-in names are skipped. Through `bootStack`,
with `OS_METADATA_WRITABLE=position`, environment saves under
`org_admin` and `everyone` answered `200`, and the restart boots, with
`GET /api/v1/meta/position/org_admin` answering the saved definition.
S2b's pins are green: `builtin-positions.boot.test.ts` is in the
plugin-security suite below.
- **M5, the legacy shape.** The save door refuses it now (`PUT
/api/v1/meta/permission/NAME` over a package-held set answers `403`,
with or without `?package=`), so the rows were written at the driver. A
row bound to no package refuses the restart, naming both holders
(pinned). So does a row bound to the package itself (`package_id` = the
package; objectql pin). A hot install refuses that bound row alike:
measured, holder `environment`. A legacy row over one of the platform
security plugin's own permission sets (`member_default`) refuses the
restart, naming `com.objectstack.plugin-security`. On base, all three
boot.
- **M6, capabilities.** `PUT /api/v1/meta/capability/NAME` answers `403`
("code-only … allowRuntimeCreate=false"), so the environment catalog
holds no capability. The check reads permission sets and positions only,
and no capability path reaches it.
## Door table: base vs head
"Base" is the untouched 28bff18, or a15b8af with the check
ablated, as each row says. "Head" is 72dcb8e (3c160a2 changes
comments only). Boots go through `@objectstack/verify`'s `bootStack` on
one database file unless the row says otherwise.
| Door | Base | Head |
|---|---|---|
| Cold boot: environment-saved permission set and position, then a
package declaring both | boots; two `[Registry] Collision` warnings; the
by-name read answers the environment's definitions (28bff18 and
ablated) | refused: `Plugin com.objectstack.engine.objectql failed to
start`, cause `422 NAMESPACE_CONFLICT`, two conflicts, incoming the
package, holder `environment` |
| Hot install (post-boot `manifest.register`) of that package | refused,
`422`, holder `environment`, both names | unchanged |
| Artifact boot (`createStandaloneStack`, `file:` database), a package
added over environment-saved names | boots (ablated: runtime pin red) |
refused, same envelope |
| Built-in shadow: environment saves under `org_admin` and `everyone`,
restart | boots (ablated) | boots; the stored definition answers |
| Legacy row (written at the driver, bound to no package) over a
package-held set and position, restart | boots, one collision warning
(28bff18) | refused, holder `environment`, both names |
| Legacy row bound to the package itself, restart | boots (ablated) |
refused, holder `environment` |
| Legacy row over the platform's `member_default`, restart | boots
(ablated) | refused, incoming `com.objectstack.plugin-security` |
| Same-package restart; a package whose names the environment does not
hold | boots | boots |
| Remedy: boot without the package, `DELETE
/api/v1/meta/permission/NAME` and `/position/NAME`, boot with it | (n/a)
| both `200`, no row left, the boot with the package comes up |
| Environment save of a capability | `403` code-only | unchanged |
## In-repo census
The examples ship no `sys_metadata` rows, so the environment catalog
holds no names on a fresh boot. Measured on a15b8af: a fresh boot of
each example on a database file, then a restart.
| Example | Package-held items | Environment rows
(`permission`/`position`) after the boot | Restart |
|---|---|---|---|
| `app-crm` | 10 permission sets, 9 positions | 0 | boots |
| `app-showcase` | 17 permission sets, 16 positions | 0 | boots |
| `app-multi-package` | 8 permission sets, 6 positions | 0 | boots |
The counts include the platform's own items (`plugin-security`'s 8
permission sets and 6 built-in positions). Names held twice: 0.
`app-todo` declares no catalog name (#22197's census) and is not a
dogfood dependency, so it was not booted. Deployed environments: NOT
MEASURED.
## Tests
The head is 3c160a2. Against 72dcb8e it changes comment lines
only, in the new dogfood file (5 added, 3 removed, 0 outside a `//`
comment). The runs below are at 72dcb8e or earlier, as each line
says.
- `@objectstack/objectql`, whole suite at e3ae92a: 387 files / 7615
passed. At 72dcb8e, `protocol-boot-hydration-scoped.test.ts`: 16
passed (8 of them new).
- `@objectstack/plugin-security`, whole suite at e3ae92a: 184 files /
3869 passed, 45 skipped. That includes S2b's
`builtin-positions.boot.test.ts` and
`bootstrap-declared-positions.test.ts`.
- `@objectstack/runtime`, whole suite at e3ae92a: 340 files / 4777
passed, 19 skipped.
`standalone-stack-security-catalog-one-holder.test.ts` has 6, 1 of them
new.
- Dogfood, the CI split, at e3ae92a:
- 1/3: 76 files / 567 passed;
- 2/3: 75 files passed and 1 failed (539 tests, 1 failed, 1 skipped);
- 3/3: 75 files passed and 1 skipped (669 passed, 8 skipped).
The one red was this PR's own built-in control: its `PUT
/api/v1/meta/position/org_admin` answered `403` with the hatch set. The
protocol memoises `OS_METADATA_WRITABLE` at its first read in a process,
and the control set it only after the file's first case had already
saved through the metadata door. It passed in isolation before the
second merge and failed in the full shard after it; what made that
difference is NOT MEASURED. At 72dcb8e the file opens the hatch
before its first boot. The new file and the re-shaped Discard Overlay
file then ran: 2 files / 11 passed.
- Before the second merge, at bdfba35: dogfood 1/3 76 files passed;
2/3 75 passed and 1 failed (the Discard Overlay file, re-shaped since);
3/3 74 passed and 1 skipped.
- `typecheck` at 72dcb8e: `objectql` (`tsc --noEmit` plus
`check:test-typecheck`: 40 files, 234 errors, 65 pinned signatures, no
new signature) and `dogfood`, exit 0. `runtime` at e3ae92a, exit 0;
no runtime file changed after it.
- `pnpm exec eslint --no-inline-config --format json` over the 7 touched
TypeScript files at 72dcb8e: 7 files, 0 errors, 0 warnings. This
narrowed run is a measurement, not a skipped one, on three grounds:
- the population comes from `eslint.config.mjs` itself (`files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` minus `NEVER_LINTED`), and all
7 files are in it;
- the count, 7, is read from the JSON output;
- the config enables no type-aware linting (no `parserOptions.project`,
as stated at `eslint.config.mjs:328`), so this diff cannot move any
untouched file's verdict.
The whole-repo `pnpm lint` is CI's.
## Ablation
The call was neutralised through `scripts/ablation-replace.mjs`, which
wraps the run and restores on exit. In `plugin.ts`,
`this.refuseEnvironmentHeldSecurityCatalogNames();` became the same call
behind an always-false guard carrying the marker
`ABLATION_22307_MARKER`, so the method stays referenced and the DTS
build still runs.
- **Landed on disk:** anchor 1 → 0, replacement 0 → 1, blob
`399ddf47c127` → `2cf40c49e6e2`. `objectql` was rebuilt (exit 0), and
`ablation-dist-preflight` found the marker in 2 built files.
- **objectql pins (from `src`):** 5 failed / 11 passed of 16 in
`protocol-boot-hydration-scoped.test.ts`. All 5 refusal pins went red:
per type, the environment-held name and the row bound to the package,
plus every conflict in one refusal. The controls stayed green: distinct
names per type, and a built-in name the platform declares beside a
stored definition.
- **runtime pins (from `dist`):** 1 failed / 5 passed. The artifact-boot
case went red; #22197's five stayed green.
- **dogfood pins (from `dist`):** 2 failed / 2 passed. The cold-boot
case and the legacy-row case went red; the built-in shadow and
distinct-name controls stayed green.
- **Base readings under ablation** (an uncommitted probe): the cold boot
booted with two collision warnings; the row bound to the package booted
cold and was refused hot; the `member_default` overlay booted; S2b
booted.
- **Restore:** blob back to `399ddf47c127` == HEAD, `git diff HEAD`
empty, `git status --porcelain` empty. After a rebuild,
`ablation-dist-preflight --absent` is green: the marker is absent from
all 14 built files and the tree is clean.
The ablation ran at a15b8af. The second `main` merge (e3ae92a)
brought #22331's `plugin.ts` hunks, none of them on this check's lines,
and the refusal pins were re-run green at 72dcb8e.
## Clause-② (measured on the built entry declarations at 72dcb8e)
`packages/objectql/dist/{index,core}.d.ts` and the shared chunk declare
no new exported name. `findEnvironmentHeldSecurityCatalogNames`,
`ENVIRONMENT_HELD_SECURITY_CATALOG_TYPES` and
`SecurityCatalogNameConflictError` are absent from the entries' export
lists. The only new declaration text is three private member names
(`SchemaRegistry.environmentHeldSecurityCatalogConflicts`,
`SchemaRegistry.securityCatalogPackageHolders`,
`ObjectQLPlugin.refuseEnvironmentHeldSecurityCatalogNames`) plus JSDoc.
No widening was found, so `Clause-②: no` stands.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands` derived 81 commands at
the head, 3c160a2. All 81 ran with exit codes recorded, and `--ran`
reconciles 81/81 with 0 NOT-MEASURED (a derived zero). 80 exited 0. The
same 81 were derived and run at 72dcb8e, with the same answers.
One exited 1, by design: `check-empty-changeset --base origin/main`. It
is the deliberate correction of #22135's pending note (see Acceptance
notes), and the gate's own text says to confirm that class on the PR,
not restore the note.
On e3ae92a, `check:dual-build-cjs-loads` first answered PREREQUISITE
NOT MET (exit 3) until eight packages outside this change were built:
`studio`, `client-react`, `embedder-openai`, `knowledge-memory`,
`knowledge-ragflow`, `organizations`, `service-cluster-redis` and
`service-knowledge`. On 72dcb8e and 3c160a2 it exits 0.
The changeset gates: `check-changeset-no-major --base` exits 0 (pre mode
`next`), `check:adr-0087-registration` exits 0, and
`check:changeset-gate-self-tests` exits 0.
CI's own lanes are declared to CI and are NOT MEASURED here: the Test
Core shards, Temporal Conformance, Dogfood Verify CLI, Build Core and
the workspace type-check lanes. `origin/main` is 7 commits ahead of the
head, among them #22352 (`plugin-security` grant readers) and #22353
(`metadata-protocol` seed loader); none touches a file of this PR. `git
merge-tree` against it is clean, so `main` was not merged again.
## Acceptance notes
- **A pending release note this PR corrects (`check-empty-changeset`
stays red by design).**
`.changeset/22135-security-catalog-one-holder.md` is #22135's pending
note, not yet consumed by a release (`packages/objectql` is at
`17.7.0`). Its "What is NOT refused" paragraph said a package added at
cold boot over an environment-held name "is not refused at cold boot".
On this PR's merge that sentence is false, and both notes would publish
in the same release. That one sentence now says the door cannot see the
name at cold boot, and that the engine checks it right after the
environment catalog loads and refuses the boot. Nothing else in the note
changed. The gate's own text names this shape a DELIBERATE CORRECTION,
to be confirmed on the PR, not restored. If a release consumes the note
before this PR lands, the edit no longer reaches a published CHANGELOG,
and the correct move then is an erratum PR against that CHANGELOG entry.
- **The 2026-08-24 legacy-overlay remedies lose their boot-time
population for code-package-declared sets.** The overlay detection
reading and the drift pass's `overlay_shadow` run in `plugin-security`'s
`kernel:ready`. A boot carrying an environment overlay of a
package-declared set is now refused before `kernel:ready`, so on a
deployment that boots, those branches see no such overlay. The same
holds for the Discard Overlay action's discard path for such a set. The
ruling names this cost ("including rows saved before the packaged
locks"). The upgrade route is in the changeset: rename, or remove the
row. A deployment can also run Discard Overlay on the release it runs
now, before upgrading.
`permission-set-discard-overlay-eligibility.dogfood.test.ts` (#21860's
pin) wrote its legacy overlay before a cold boot, which is now refused.
It now writes the overlay into the running deployment and runs the two
passes the boot ran for it, by the functions the security plugin's boot
calls (`reconcilePermissionSetProjection`, then the drift pass), so its
preconditions and its control still hold.
- **The refusal leaves `start()`, so the kernel wraps it.**
`bootstrap()` rejects with `Plugin com.objectstack.engine.objectql
failed to start - rollback complete: …`, and the envelope is the
wrapper's `cause`, as with any `start()`-time refusal (#22197's
item-seam refusal from `plugin-security.start` included). The pins read
`cause`.
- **Org-scoped rows are not judged.** Boot hydration loads env-wide rows
only (`organization_id IS NULL`), and org-scoped rows never reach the
registry, so the check judges the env-wide catalog. That is the
population hydration serves.
- **A refused boot over a `sqlite-wasm` file can still flush after the
refusal.** In a probe, removing the database directory right after the
refused `bootStack` raised `ENOENT` from the driver's atomic write. The
committed dogfood file keeps its database files in the test file's
working directory, which the dogfood run removes at its end, and never
boots a file again after it was refused. Noted, not filed: a boot that
failed has no process left to serve.
- **Files outside the engine lane:**
-
`packages/qa/dogfood/test/security-catalog-cold-boot-environment-holder.dogfood.test.ts`
(new) and
`packages/qa/dogfood/test/permission-set-discard-overlay-eligibility.dogfood.test.ts`
(re-shaped, above): `domain:cli`.
-
`packages/runtime/src/standalone-stack-security-catalog-one-holder.test.ts`
(one case added, and the artifact-stack helper takes a `databaseUrl`):
`domain:cli`.
-
`scripts/adr-anchors/packages__objectql__src__security-catalog-namespace.ts.json`.
- `.changeset/22135-security-catalog-one-holder.md` (above).
## Patch round 1 — the release note's remedy, completed
Both contract reviews passed: 6070947709 on this PR, which also confirms
the correction of #22135's pending note, and 6070955792 on the ADR PR.
This round changes text only. The code, the pins and
`.changeset/22135-security-catalog-one-holder.md` are unchanged. The
head is cf1a9dd.
- **`.changeset/22307-cold-boot-catalog-refusal.md`.** "The upgrade
shape" names the legacy plural types. "The one-line fix" now has three
parts:
- **Before upgrading, for a permission set.** The `kernel:ready` overlay
reading names the sets this release refuses. The audited Discard Overlay
action, or `DELETE /api/v1/meta/permission/NAME`, removes each overlay
without touching the database, including on the platform's own sets.
- **After upgrading, for a package that can be left out.** Boot without
it, then delete through the metadata API.
- **After upgrading, for a name the platform security plugin declares.**
The SQL delete of the active, environment-wide rows under the type or
its legacy plural.
The changeset also says that no `os` command deletes a `sys_metadata`
row offline.
- **`content/docs/permissions/permission-sets.mdx`.** One clause under
"Declared ≠ enforced", on the Discard Overlay remedy: discard such an
overlay before you upgrade, because a deployment that still holds one
does not boot.
**Measured, clause by clause:**
- **The current release.** This branch with the check ablated through
`scripts/ablation-replace.mjs` (blob `9b18363e90ef` → `b3701fcc3a70`,
marker in `dist/`), a legacy `member_default` overlay written at the
driver, then a restart:
- The boot logged one `kernel:ready` warning, "[security] 1
package-declared permission set(s) are being shadowed by an environment
overlay — … use the audited "Discard Overlay" action on it …", naming
`member_default`.
- The record read `drift_status: overlay_shadow`, and Discard Overlay
answered `200` and left no active row.
- On the same release, `DELETE /api/v1/meta/permission/viewer_readonly`
over a legacy overlay of that platform set answered `200`
("Customization overlay deleted — permission/viewer_readonly reset to
artifact default") and left no active row. So Discard Overlay is not the
only database-free remedy before the upgrade; the changeset names both.
- **The restore.** `ablation-replace` put the blob back (== HEAD, `git
diff HEAD` empty). After the rebuild, `ablation-dist-preflight --absent`
was green on `dist/` at once. It was green on the tree once this round's
doc edit, the one dirty path at that moment, was committed (cf1a9dd).
- **The head, check live:**
- The database on which the current release ran Discard Overlay on
`member_default` boots.
- Rows of type `permissions` and `positions` (the legacy plurals) over
package-held names refuse the restart, both named.
- A `draft` row over a third package-held name is not loaded and not
named.
- `loadMetaFromDb` selects `state: 'active'` and `organization_id:
null`, and folds the type through `PLURAL_TO_SINGULAR`, which maps
`permissions` to `permission` and `positions` to `position` on `main`.
It sets no `package_id` condition: a row bound to the package itself
refuses too, measured in the first round.
- **The SQL.** The changeset's `DELETE` statements, run through Python's
`sqlite3` against the refused database files (one per type, and one for
`member_default`), deleted 1 row each. Each restart then booted.
- **The CLI.** `os meta delete` and `os data delete` build an API client
and require a token (`createApiClient`, `requireAuth`), and no command
under `packages/cli/src/commands` deletes a `sys_metadata` row.
- **The action.** `discard_permission_set_overlay`, labelled "Discard
Overlay", on `sys_permission_set`, in the list-item and record-header
locations, visible while `drift_status` is `overlay_shadow`. It is
documented on `content/docs/permissions/permission-sets.mdx` under
"Declared ≠ enforced — diagnosing a frozen package set". Positions have
no overlay reading (it reads the `permission` / `permissions` types) and
no such action.
- **NOT MEASURED:** the metadata API delete on a set a non-platform
package ships, and a position overlay before upgrading.
**Gates at cf1a9dd.** `dispatch-gates --commands` derived 107
commands; the doc page added the docs families. All 107 ran with exit
codes recorded, and `--ran` reconciles 107/107 with 0 NOT-MEASURED. 106
exited 0, including `check-changeset-no-major --base`,
`check-adr-0087-registration --base`, `check:doc-authoring`,
`check:docs-*`, `check-doc-frontmatter`, `@objectstack/spec`'s
`check:docs` and `check:doc-formula-expressions`. One exited 1 by
design: `check-empty-changeset --base origin/main`, the confirmed #22135
correction. `origin/main` is 12 commits ahead; `git merge-tree` against
it is clean, so `main` was not merged.
**One more file outside the engine lane:**
`content/docs/permissions/permission-sets.mdx` (`domain:devx`).
## Patch round 2 — the metadata-API delete reaches singular-typed rows
only
The at-tier contract review on cf1a9dd (6071828819) failed two remedy
sentences, and judged everything else right: the code, the #22135
correction (confirmed on that head), case 3's SQL, the CLI sentence, the
docs clause and the semver. The two sentences are case 1's "So does
`DELETE /api/v1/meta/permission/NAME`" and case 2's metadata-API delete.
Both are false for a row stored under the legacy plural `permissions` /
`positions`, a shape the changeset's own "upgrade shape" paragraph
names. This round changes
`.changeset/22307-cold-boot-catalog-refusal.md` only. No code, pin, docs
page or `.changeset/22135-security-catalog-one-holder.md` change. The
head is 39ef237.
**Measured first; the review's reading holds.**
- **The current release** (this branch with the check ablated through
`scripts/ablation-replace.mjs`, blob `9b18363e90ef` → `b3701fcc3a70`,
marker in `dist/`):
- A legacy overlay of `viewer_readonly` stored under `permissions`:
`DELETE /api/v1/meta/permission/viewer_readonly` answered `200` with
`{"success":true,"reset":false,"message":"No customization overlay found
for permission/viewer_readonly — already at artifact default."}`, and
the `permissions` row stayed active. Discard Overlay on the same set
answered `200` and left no active row.
- `mcp_agent_restricted` with two active rows, one bound to no package
and one bound to `com.objectstack.plugin-security`: the first `DELETE`
answered `200` "Customization overlay deleted — … reset to artifact
default" and removed one row, leaving the bound one. A second `DELETE`
removed it.
- **The head, check live, case 2.** A package's permission set and
position stored under `permissions` / `positions`. Booted without the
package, `DELETE /api/v1/meta/permission/pr2_set` answered `200` "No
permission 'pr2_set' found — nothing to delete.", and `DELETE
/api/v1/meta/position/pr2_pos` answered "No position 'pr2_pos' found —
nothing to delete." Both rows stayed active, and the boot with the
package added back was refused, both names held by `environment`.
- **The restore.** Blob == HEAD and `git diff HEAD` empty. After the
rebuild, `ablation-dist-preflight --absent` is green on `dist/` and on
the tree.
**The text fix, as the record names it:**
- **Case 1:** "neither touches the database" now reads "neither needs
direct database access".
- **Case 3's heading** now reads "for a name the platform security
plugin declares, or for any row the metadata API does not reach".
- **One paragraph after the three cases**, before the CLI sentence:
- the two `DELETE` routes reach a row stored under `permission` or
`position` only, one row per call;
- a plural-typed row is not reached: `200`, nothing found, nothing
removed;
- where a name has two active rows, each call removes one;
- a plural-typed row is removed by Discard Overlay before upgrading (a
permission set), or by the SQL above after upgrading, for any name.
This also corrects round 1's summary above: the metadata-API delete is a
database-free remedy before the upgrade only for a row stored under the
singular type.
- `content/docs/permissions/permission-sets.mdx`'s clause does not name
the metadata-API delete, so the page is unchanged.
**Gates at 39ef237.** `dispatch-gates --commands` derived 107
commands. All 107 ran with exit codes recorded, and `--ran` reconciles
107/107 with 0 NOT-MEASURED. 106 exited 0; one exited 1 by design:
`check-empty-changeset --base origin/main`, the confirmed #22135
correction. `origin/main` is 22 commits ahead. `git merge-tree` against
it is clean, so `main` was not merged.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0ef9029 commit e030d43
11 files changed
Lines changed: 705 additions & 43 deletions
File tree
- .changeset
- content/docs/permissions
- packages
- objectql/src
- qa/dogfood/test
- runtime/src
- scripts/adr-anchors
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
| 25 | + | |
26 | 26 | | |
27 | 27 | | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
371 | | - | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
372 | 374 | | |
373 | 375 | | |
374 | 376 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
38 | 41 | | |
39 | 42 | | |
40 | 43 | | |
| |||
935 | 938 | | |
936 | 939 | | |
937 | 940 | | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
938 | 948 | | |
939 | 949 | | |
940 | 950 | | |
| |||
2106 | 2116 | | |
2107 | 2117 | | |
2108 | 2118 | | |
| 2119 | + | |
| 2120 | + | |
| 2121 | + | |
| 2122 | + | |
| 2123 | + | |
| 2124 | + | |
| 2125 | + | |
| 2126 | + | |
| 2127 | + | |
| 2128 | + | |
| 2129 | + | |
| 2130 | + | |
| 2131 | + | |
| 2132 | + | |
| 2133 | + | |
| 2134 | + | |
| 2135 | + | |
| 2136 | + | |
| 2137 | + | |
| 2138 | + | |
| 2139 | + | |
| 2140 | + | |
| 2141 | + | |
| 2142 | + | |
| 2143 | + | |
| 2144 | + | |
| 2145 | + | |
| 2146 | + | |
| 2147 | + | |
| 2148 | + | |
| 2149 | + | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
| 2153 | + | |
| 2154 | + | |
| 2155 | + | |
2109 | 2156 | | |
2110 | 2157 | | |
2111 | 2158 | | |
| |||
0 commit comments