From bd9a25d363316bebf6f9566e361c6d609b35e0fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 07:37:58 +0000 Subject: [PATCH] docs(plugin-security): restate the upsertEnvPermissionSet JSDoc for ADR-0094 D5-R (#7082) PR #6962 retired the 2026-07-14 env-overlay direction and fixed this file's header; the function-level JSDoc of `upsertEnvPermissionSet` kept the same two claims D5-R retired -- "an env overlay is the platform's standard customization of a packaged definition" and "deleting the overlay resets the row to the shipped declaration". #6483 / PR #6608 rolled `permission` back to `allowOrgOverride: false` (the first is 403 NOT_OVERRIDABLE) and #6960 measures the delete path refusing to lift even a legacy overlay (the second). Prose only, no behaviour change. Three neighbouring copies in the same package are corrected with it so the package no longer states the direction in two voices. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ --- .../permission-set-projection-d5r-jsdoc.md | 27 +++++++++ .../src/permission-set-projection.test.ts | 9 ++- .../src/permission-set-projection.ts | 57 ++++++++++++++++--- .../src/security-plugin.test.ts | 13 +++-- .../plugin-security/src/security-plugin.ts | 29 ++++++---- 5 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 .changeset/permission-set-projection-d5r-jsdoc.md diff --git a/.changeset/permission-set-projection-d5r-jsdoc.md b/.changeset/permission-set-projection-d5r-jsdoc.md new file mode 100644 index 0000000000..f744251eaa --- /dev/null +++ b/.changeset/permission-set-projection-d5r-jsdoc.md @@ -0,0 +1,27 @@ +--- +"@objectstack/plugin-security": patch +--- + +Docs: bring the second ADR-0094 copy in `permission-set-projection.ts` up to D5-R. + +PR #6962 retired the 2026-07-14 "customize packaged permission sets through an ADR-0005 +env overlay" direction and corrected this file's **header**. A second copy survived in +the function-level JSDoc of `upsertEnvPermissionSet` — an exported symbol, so the stale +text ships in the published `.d.ts` and reads as fact to the next author. It stated both +halves D5-R retired: that an env-scope overlay is the platform's standard customization +of a packaged definition, and that deleting the overlay resets the row to the shipped +declaration. + +Both are now stated as current: `#6483` (PR #6608) rolled `permission` back to +`allowOrgOverride: false`, so a metadata write against a code-declared (artifact-backed) +set is refused by the producer with 403 `NOT_OVERRIDABLE` and the supported channel is +ADR-0086's (edit the package, re-publish); and `#6960` measures the ordinary delete path +refusing to lift even a legacy pre-rollback overlay, leaving `OS_METADATA_WRITABLE` as +the only documented removal — so "delete = reset" is recorded as retired rather than +restated. The retirement itself is kept in the text, not deleted, so a reader arriving +at this function does not have to reconstruct the history. + +Prose only — no behaviour change. The same retired direction was also corrected in three +neighbouring comments in this package (the `readDeclaredBody` JSDoc, and the two +`security-plugin.ts` package-managed write-gate comments) plus their two test rationales, +so the package no longer states the direction in two voices. diff --git a/packages/plugins/plugin-security/src/permission-set-projection.test.ts b/packages/plugins/plugin-security/src/permission-set-projection.test.ts index 18f5f56deb..655545d298 100644 --- a/packages/plugins/plugin-security/src/permission-set-projection.test.ts +++ b/packages/plugins/plugin-security/src/permission-set-projection.test.ts @@ -333,9 +333,12 @@ describe('upsertEnvPermissionSet (ADR-0094 — record is a pure projection)', () }); it('projects onto a PACKAGE-OWNED row (overlay customization) while preserving its provenance', async () => { - // Direction confirmed 2026-07-14: an env overlay of a packaged set is the - // platform's standard ADR-0005 customization — the record follows the - // effective body; the package still owns the row. + // Unit-level shape, unchanged by ADR-0094 D5-R: handed a body for a + // package-owned row, this function projects the facets and preserves the + // provenance. What D5-R retired is the CLAIM about where that body comes + // from — an env overlay of a packaged set is no longer a supported + // customization channel (#6483 / PR #6608); see the D5-R lifecycle block + // below for the refusal this projector now sits behind. const ql = makeQl(); ql.permRows.push({ id: 'ps_pkg', name: 'organization_admin', managed_by: 'package', package_id: 'com.example.crm', system_permissions: '["pkg"]' }); const r = await upsertEnvPermissionSet(ql, envBody()); diff --git a/packages/plugins/plugin-security/src/permission-set-projection.ts b/packages/plugins/plugin-security/src/permission-set-projection.ts index 448b9fd799..1cf282840a 100644 --- a/packages/plugins/plugin-security/src/permission-set-projection.ts +++ b/packages/plugins/plugin-security/src/permission-set-projection.ts @@ -287,8 +287,11 @@ const isProjectionEcho = (v: any): boolean => * Read the DECLARED (artifact) body for a permission set from the engine's * SchemaRegistry — the same source `bootstrapDeclaredPermissions` seeds from, * and the one store the env projection never writes, so it can't be poisoned - * by our own registry sync. Used as the reset target when an env overlay is - * deleted off a declared set. + * by our own registry sync. It is the body for a declared set with no overlay + * at all, and the reset target when an overlay IS lifted off one — which since + * ADR-0094 D5-R means a LEGACY (pre-#6483) row removed through the operator + * hatch, not a data-door delete: #6960 measures that delete refusing with 403 + * `NOT_OVERRIDABLE`. * * Items tagged `_packageId: 'sys_metadata'` are RUNTIME SHADOWS — hydrated * into the registry from overlay rows (loadMetaFromDb / getMetaItems), not @@ -328,13 +331,49 @@ function hasSchemaRegistry(ql: any): boolean { * (`managed_by:'admin'` — A4 #2920 unified vocab, formerly 'user'; a * Studio-authored set appears in Setup, where the * #2867 band-aid declined to create). A PACKAGE-OWNED row is also projected — - * an env-scope overlay is the platform's standard customization of a packaged - * definition (ADR-0005; direction confirmed 2026-07-14, reversing the earlier - * refuse-the-env-door rule): the facets update to the EFFECTIVE (overlay-wins) - * body while the `managed_by:'package'` + `package_id` provenance is - * PRESERVED — the row still belongs to the package; the overlay is a - * customization of it, and deleting the overlay resets the row to the shipped - * declaration (the layered read reveals the baseline again). + * its facets follow the body this pass is handed, while the + * `managed_by:'package'` + `package_id` provenance is PRESERVED: the row + * still belongs to the package. + * + * WHICH body that is, is the part this comment used to get wrong. The + * 2026-07-14 direction confirmation that used to sit here — "an env-scope + * overlay is the platform's standard ADR-0005 customization of a packaged + * definition, and deleting the overlay resets the row to the shipped + * declaration" — is **RETIRED**, in BOTH halves (ADR-0094 D5-R, 2026-08-09; + * #6609 ruling A executed by #6858; the file header above records the same + * retirement). Since #6483 / PR #6608 rolled `permission` back to + * `allowOrgOverride: false`: + * + * - **no new overlay of a packaged set can be minted.** A metadata write + * against a CODE-DECLARED (artifact-backed) set — `*.permission.ts`, a + * stack's `permissionSets` — is refused by the producer with 403 + * `NOT_OVERRIDABLE`, so for those names the body reaching here is the + * DECLARED one and the projected facets ARE the shipped declaration. The + * supported way to change them is the one ADR-0086 always named: edit the + * package and re-publish. What survives is the neighbouring + * `allowRuntimeCreate` tier (still `true`) — a set whose definition lives + * only in `sys_metadata`, created through the data door or authored and + * published through the METADATA door (ADR-0070). That tier edits the + * single stored definition IN PLACE: no code-vs-overlay layering, and + * nothing to reset to (ADR-0094 D5-R calls it the surviving neighbour, not + * D5's successor); + * - **`customized` therefore badges LEGACY state, not a supported channel.** + * `supportsOverlay` is unchanged, so an overlay row authored BEFORE the + * rollback still merges overlay-wins at read time + * ({@link projectPermissionMutation} hands us `overlay ?? declared`) and + * this pass still stamps the flag for it. The in-repo corpus had zero such + * rows when PR #6608 measured it; + * - **"delete = reset" must NOT be read back into that.** #6960 measures the + * ordinary delete path refusing to lift exactly such a legacy overlay: on + * an environment-scoped kernel `deleteMetaItem` throws `NOT_OVERRIDABLE` / + * 403 for an artifact-backed target of a non-overridable type BEFORE it + * probes for the row, and a kernel with no `environmentId` refuses the + * same write as `override-artifact` intent — leaving the operator hatch + * (`OS_METADATA_WRITABLE=permission`) as the only documented removal. With + * no overlay to lift — the normal case — the delete is a no-op success and + * the row keeps projecting the declaration. {@link readDeclaredBody} stays + * the correct reset target IF an overlay is ever lifted; it is no longer a + * path the data door can walk. */ export async function upsertEnvPermissionSet( ql: any, diff --git a/packages/plugins/plugin-security/src/security-plugin.test.ts b/packages/plugins/plugin-security/src/security-plugin.test.ts index bc8392c066..a8d53a255f 100644 --- a/packages/plugins/plugin-security/src/security-plugin.test.ts +++ b/packages/plugins/plugin-security/src/security-plugin.test.ts @@ -1986,11 +1986,14 @@ describe('SecurityPlugin', () => { return harness.run(opCtx); }; - it('PASSES an admin update of a package-managed set (ADR-0094: the write-through turns it into an env overlay)', async () => { - // Direction confirmed 2026-07-14: update/delete on a package row are no - // longer refused at this gate — the ADR-0094 write-through downstream - // translates them into env-scope overlay operations (customize / reset). - // The single-store refusal lives in the write-through itself, covered in + it('PASSES an admin update of a package-managed set at THIS gate (ADR-0094: the refusal is the write-through producer\'s, not this gate\'s)', async () => { + // update/delete on a package row are not refused at this gate — the + // ADR-0094 write-through downstream translates them into a metadata + // write, and that producer decides. Since ADR-0094 D5-R (#6483 / + // PR #6608) the answer for a CODE-DECLARED set is 403 NOT_OVERRIDABLE, + // so "the write-through turns it into an env overlay" is no longer why + // this passes; it passes because the gate's job is forging provenance, + // not overridability. The refusal is covered in // permission-set-projection.test.ts. const opCtx: any = { object: 'sys_permission_set', operation: 'update', diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index ca83fc562b..b15c0ea552 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -887,10 +887,12 @@ export class SecurityPlugin implements Plugin { // update), and the lifecycle ops with no overlay translation // (transfer/restore/purge) stay refused on package rows. Ordinary // `update`/`delete` on a package row are handled downstream by the - // ADR-0094 write-through, which translates them into env-scope OVERLAY - // operations (customize / reset via the standard ADR-0005 layering) — - // the boot re-seed can no longer revert an admin's change, because the - // change lives in the overlay and the record projects overlay-wins. + // ADR-0094 write-through, which TRANSLATES them into a metadata write. + // Whether that write is ACCEPTED is ADR-0005's call, not this gate's, + // and since ADR-0094 D5-R (#6483 / PR #6608 rolled `permission` back to + // `allowOrgOverride: false`) a CODE-DECLARED set is refused there with + // 403 `NOT_OVERRIDABLE` — the 2026-07-14 "customize / reset via an env + // overlay" direction this comment used to state is RETIRED. // Placed BEFORE the empty-principal fall-open and the CRUD check so the // forging boundary holds even for a principal-less context and a // superuser with modifyAllRecords. System/boot writes carry `isSystem` @@ -3189,14 +3191,17 @@ export class SecurityPlugin implements Plugin { } if (op === 'insert') return; // no existing row to protect - // [ADR-0094, direction confirmed 2026-07-14] `update`/`delete` on a - // package-managed row are no longer refused here: the write-through - // middleware (which runs after this gate + the delegated-admin gate + - // the CRUD checks) translates them into env-scope OVERLAY operations — - // customize / reset via the standard ADR-0005 layering — and itself - // re-asserts the legacy refusal when the kernel has no metadata overlay - // layer to carry the customization. The lifecycle ops below have no - // overlay translation, so the package-row protection stays for them. + // [ADR-0094 D5-R] `update`/`delete` on a package-managed row are not + // refused HERE: the write-through middleware (which runs after this gate + // + the delegated-admin gate + the CRUD checks) translates them into a + // metadata write, and the refusal is LEFT TO THAT PRODUCER. Since #6483 / + // PR #6608 rolled `permission` back to `allowOrgOverride: false`, a + // CODE-DECLARED (artifact-backed) set is refused there with 403 + // `NOT_OVERRIDABLE`; a `sys_metadata`-backed set rides + // `allowRuntimeCreate` and still lands. The 2026-07-14 "customize / reset + // via the standard ADR-0005 layering" direction this comment used to cite + // is RETIRED. The lifecycle ops below have no metadata translation, so the + // package-row protection stays for them. if (op === 'update' || op === 'delete') return; if (!this.ql) return;