Skip to content

Commit 11d119a

Browse files
docs(permissions): describe the packaged permission-set lock in "One authoritative store" (#22391)
Fixes #22379 Clause-②: no ## What changed Two docs pages: four passages that still said a set shipped in code can be overlaid in the environment. +25 / -16 lines against `main`. ### `content/docs/permissions/permission-sets.mdx`, "One authoritative store — the record is a projection (ADR-0094)" - **First bullet.** It said a Setup edit of any declared set, packaged sets included, "becomes an environment overlay" that "genuinely takes effect", and that the Studio layered view diffs it and a reset removes it. Since the packaged lock, that edit answers `403 NOT_OVERRIDABLE` and stores nothing. The bullet now says: - which sets are locked: a set shipped in code, meaning a set an installed package declares (`*.permission.ts`, a stack's `permissionSets`) or a platform default such as `member_default`; - what the edit answers (`403 NOT_OVERRIDABLE`, nothing stored); - the remedy the refusal names: clone to a new name, with the **Clone** action or `POST /api/v1/data/sys_permission_set`; - which sets are still edited in place: one created in this environment, a clone, or one saved into a writable runtime package. - **Closing sentence.** "use an overlay where a packaged set must be narrowed" becomes: clone it, narrow the clone, and bind the clone in its place (restriction is done by not granting). Narrowing the packaged set itself is the package author's change (ADR-0086 two-doors). The position-binding half of the sentence did not change. - **Third bullet** ("Deleting through the data door … resets it"). It holds against `main`, so it is **not edited**. Evidence is under premise check 5. ### `content/docs/permissions/permission-sets.mdx`, "Provenance — package vs environment sets (ADR-0086)", `:311-312` - Before: "(two-doors separation, evolved by ADR-0094 — ordinary edits of a packaged set become environment overlays, see below)". - After: "(two-doors separation, evolved by ADR-0094 — an edit of a set shipped in code is refused; clone it instead, see below)". - Why: its "see below" pointed at the corrected bullet, which now says the opposite. - Backed by the same pins as the bullet: `permission-set-write-through-package-binding.dogfood.test.ts:167-173` (a data-door edit of `showcase_contributor` answers `403 NOT_OVERRIDABLE`, no row minted), and `permission-set-lock-row-provenance.dogfood.test.ts` (the shipped set is refused at both doors, while a clone takes an edit). The clone remedy is the `clone_permission_set` action in `packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts:115-160`. ### `content/docs/permissions/administrator-guide.mdx`, Step 5 (`:162-164`) and the FAQ (`:194-196`) - **Step 5.** - Before: "Editing a set that shipped with an app creates an *environment overlay* — your change wins, survives upgrades, and can be reset back to the vendor baseline; the API name is immutable after creation." - After: "A set shipped in code — by an app you installed, or as a platform default such as `member_default` — can't be edited in the environment (a save answers `403 NOT_OVERRIDABLE`): clone it and adjust the clone, which is your own set while upgrades keep reaching the original. Sets created here, clones, and sets saved into a writable runtime package are edited in place. The API name is immutable after creation." - The page's order of preference is unchanged: bind an existing set to a position, then clone and adjust, then author a new set. - **FAQ, "Can I delete the built-in permission sets?"** - Before: "Shipped sets can be overlaid (Step 5) or simply left unassigned." - After: "Shipped sets can't be edited in place either, but they can be cloned and adjusted (Step 5) or simply left unassigned." - Backed by: - The installed-app half: `permission-set-write-through-package-binding.dogfood.test.ts:167-173` and `two-doors-permission.dogfood.test.ts` 块2 (`showcase_contributor` answers 403, and no overlay is minted). - The platform-default half: `two-doors-permission.dogfood.test.ts` (last 块2 case) and `showcase-permission-projection.dogfood.test.ts` §2 pin `403` on a `member_default` edit with no overlay minted. The status is pinned. The code is read from the producer (`refusePackagedBaseOverride`, `NOT_OVERRIDABLE`) and is not pinned. - The editable populations: `permission-set-lock-row-provenance.dogfood.test.ts` shapes 1-3 (runtime-package set, org set, clone). - The upgrade sentence: the lock's own `userMessage` in `packaged-permission-set-lock.ts` ("The clone is your organization's own permission set, and package upgrades keep reaching the original."). Not touched: the "Declared ≠ enforced — diagnosing a frozen package set" section. PR #22365 (card #22307) adds a clause there in a hunk starting at `:368`, and the decision card #22371 may rewrite that section. `content/docs/releases/` is not touched either. ## Premise checks against `main` (base `117d34de3f`, now merged up to `191543456f`) 1. **The two passages were still pre-lock text.** At `117d34de3f`, `permission-sets.mdx:332-339` ("it genuinely takes effect") and `:350-352` ("use an overlay where a packaged set must be narrowed") read as the card quotes them. Holds. 2. **The Setup edit of a set a code package ships answers 403 and stores nothing.** Holds: - Pin: `packages/qa/dogfood/test/permission-set-write-through-package-binding.dogfood.test.ts:167-173`. `PATCH /data/sys_permission_set/:id` on `showcase_contributor` answers `{ status: 403, code: 'NOT_OVERRIDABLE' }`, and the active `sys_metadata` rows are unchanged. - Producer: `PackagedPermissionSetLockedError` in `packages/plugins/plugin-security/src/packaged-permission-set-lock.ts`. The data door's insert and update legs throw it (`permission-set-projection.ts`, `createPermissionSetWriteThrough`), and so does the metadata door (`packaged-permission-set-lock-gate.ts`). 3. **Scope: which package-held sets does the lock cover?** The caution was right. "A packaged set cannot be edited" over-claims, so the text is written to a narrower group: sets shipped in code. - How the lock decides: `classifyPackagedPermissionSet` reads the engine SchemaRegistry. `declaredPackageIdOf` skips projection echoes, tenant-authored stored rows (`isTenantAuthored`, `_provenance: 'org'`), and `_packageId: 'sys_metadata'` shadows. - Editable shapes, pinned: a set saved into a writable runtime package (`permission-set-write-through-package-binding.dogfood.test.ts:152-160`, and `permission-set-lock-row-provenance.dogfood.test.ts` shape 1), an org-created set (shape 2), and a clone (shape 3). 4. **Both remedies exist and work on `main`.** Holds. - **Clone:** the `clone_permission_set` action (`sys-permission-set.object.ts:115-160`) POSTs to `/api/v1/data/sys_permission_set`. It carries every permission facet but deliberately not `admin_scope`. The lock's `userMessage` and the producer's regime sentence (`packaged-base-regime.ts:162-167`) both name it. - **Bind to a position:** `sys_position_permission_set` rows ("Assigning permission sets" on this page; `positions.mdx:7-10`). `showcase-permission-zoo.dogfood.test.ts:133-143` pins a tenant admin binding the package-shipped `showcase_contributor` to a position. - Binding only adds (union) and cannot narrow anything, so narrowing goes through clone-and-bind-in-its-place. 5. **Third bullet: holds.** Measured per kind of set: - *A set created in this environment is removed:* `showcase-permission-projection.dogfood.test.ts` ("deleting a runtime-only set retires both the definition and the record") and `permission-set-projection.test.ts:785`. - *A set shipped in code is not removed, and the row remains:* `two-doors-permission.dogfood.test.ts` 块2 delete case and `showcase-permission-projection.dogfood.test.ts` §3 both answer 2xx with `success: false`, and the record stays. - *"(the customization overlay is dropped)":* a pre-lock overlay is removed by `deleteMetaItem` (the 2026-08-10 maintainer ruling, the `mergesOverlayAtRead` carve-out in `refusePackagedBaseRemoval`). Pinned by `protocol.legacy-overlay-delete.test.ts` and `permission-set-projection.test.ts:926`. With no overlay present, the delete changes nothing. - NOT MEASURED: deleting a set saved into a writable runtime package. No pin covers it. See Acceptance notes. - CI's `Dogfood Regression Gate` concluded `success` on `117d34de3f`. Confirming that each cited file appears in the job logs is NOT MEASURED: the log host answered `Forbidden` from this container. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on head `fc3fdb1b69`: 43 commands, the same list as round 1. All 43 ran on `fc3fdb1b69` and exited 0. Exit codes were captured before any pipe. - Prerequisite builds: the spec, lint and client/client-react closures were built under the verify lock (`VERDICT command-exit 0`) before the run, so the dist-reading gates measured this tree. - Reconciliation (`--ran` with per-command exit codes): "43 derived famil(ies) accounted for — 43 run, 0 NOT-MEASURED (a DERIVED zero …)". - Sample verdict lines: - `check:skill-examples`: "262 prose examples type-check across 3 surface(s)". - `check:doc-anchors`: "468 internal #fragment link(s) across 417 source file(s) all resolve to a real heading". - `check:nul-bytes`: "OK (scanned 10373 text file(s) … no raw ASCII control bytes)". - `check:doc-authoring`: clean. - `check:docs`: "225 generated files in sync with packages/spec". - `origin/main` moved to `0ef9029da4` after this round's merge. That commit only retitles `packages/spec` test files and touches no docs, so it was not merged again; CI's merge ref covers it. - Outside the derived list, for CI: the path-scheduled `Build Docs` and `Test Core` jobs, and the type-check lanes. ## Changeset None. The diff is docs-only under `content/docs/`, which no package publishes, so the PR carries the `skip-changeset` label. ## Acceptance notes Accepted by the seat as notes, not filed: - **Stale code comments, not docs.** These are comment drift with no runtime effect. - In `packages/plugins/plugin-security/src/permission-set-projection.ts:559-568`, the comment says "#6960 measures the ordinary delete path refusing to lift" a legacy overlay. The 2026-08-10 ruling since lets that delete go through (`protocol.legacy-overlay-delete.test.ts`). - The `createPermissionSetWriteThrough` doc comment at `:1055-1058` still says a package-owned row's update becomes an env-scope overlay. - **Unmeasured: deleting a runtime-package set.** This is a code reading only, not reproduced. - The data door's delete leg calls `deleteMetaItem` with no package. The repository delete matches any package (`sys-metadata-repository.ts`, `whereFor`). - `readDeclaredBody` (`permission-set-projection.ts:494`) skips `_packageId: 'sys_metadata'` shadows and projection echoes, but not the tenant-authored (`_provenance: 'org'`) rows the lock's classifier learned to skip. - A delete after a list read might therefore re-project instead of retiring. A booted-stack probe would settle it. --- _Generated by [Claude Code](https://claude.ai/code/session_0115N1oNnQS5WqofZ2DzaT3q)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 00bee27 commit 11d119a

2 files changed

Lines changed: 25 additions & 16 deletions

File tree

‎content/docs/permissions/administrator-guide.mdx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,13 @@ is a structured spreadsheet — you never hand-write JSON:
159159
- **Capabilities** — named grants like `manage_users` or `studio.access`.
160160

161161
Prefer the smallest change that works, in this order: **bind an existing set
162-
to a position** → **clone and adjust** → **author a new set**. Editing a set
163-
that shipped with an app creates an *environment overlay* — your change wins,
164-
survives upgrades, and can be reset back to the vendor baseline; the API name
165-
is immutable after creation. And keep the additive model in mind: author
162+
to a position** → **clone and adjust** → **author a new set**. A set shipped
163+
in code — by an app you installed, or as a platform default such as
164+
`member_default` — can't be edited in the environment (a save answers
165+
`403 NOT_OVERRIDABLE`): clone it and adjust the clone, which is your own set
166+
while upgrades keep reaching the original. Sets created here, clones, and sets
167+
saved into a writable runtime package are edited in place. The API name is
168+
immutable after creation. And keep the additive model in mind: author
166169
coherent capability bundles, never "subtraction sets".
167170
Full model: [Permission Sets](/docs/permissions/permission-sets).
168171

@@ -193,7 +196,8 @@ org structure, it's a business unit.
193196

194197
**Can I delete the built-in permission sets?** No — sets like
195198
`member_default` and `organization_admin` are the platform baseline. Shipped
196-
sets can be overlaid (Step 5) or simply left unassigned.
199+
sets can't be edited in place either, but they can be cloned and adjusted
200+
(Step 5) or simply left unassigned.
197201

198202
## Where is everything
199203

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ A package ships its own sets (`managedBy: 'package'` + `packageId`), seeded
309309
idempotently at boot and re-seeded on upgrade; environment-authored sets
310310
(`platform`/`user`) are never clobbered. The data layer refuses forging package
311311
provenance through the admin door (two-doors separation, evolved by ADR-0094 —
312-
ordinary edits of a packaged set become environment overlays, see below), which
312+
an edit of a set shipped in code is refused; clone it instead, see below), which
313313
is what makes package uninstall well-defined — and enforced: uninstalling a
314314
package (`DELETE /api/v1/packages/:id`) revokes its own sets, their
315315
position/user bindings, and its pending audience-binding suggestions in the
@@ -329,14 +329,16 @@ goes through the data engine) is redirected into a metadata write and the
329329
record is re-derived by an awaited projector, so the two can never drift. This
330330
has three author-visible consequences:
331331

332-
- **Editing any declared set through Setup — packaged sets included** becomes
333-
an environment overlay of that definition (the standard metadata
334-
customization): it genuinely takes effect, where a record-only edit
335-
previously displayed but never enforced. The row keeps its package
336-
provenance; the Studio layered view diffs the shipped baseline against your
337-
customization, and removing the overlay resets to the baseline. Note the
338-
trade every overlay makes: while an overlay pins a set, the vendor's later
339-
baseline changes don't take effect for it until you reset or re-author.
332+
- **A set shipped in code cannot be edited in the environment.** That is a set
333+
an installed package declares (a `*.permission.ts` file, a stack's
334+
`permissionSets`) or a platform default such as `member_default`. Editing it
335+
through Setup or the metadata API answers `403 NOT_OVERRIDABLE` and stores
336+
nothing. Clone it to a new name instead (the **Clone** action on the set, or
337+
`POST /api/v1/data/sys_permission_set` with a new name) and edit the clone:
338+
the clone is your organization's own set, and package upgrades keep reaching
339+
the original. Every other set (one created in this environment, a clone, or
340+
one saved into a writable runtime package) is edited in place: the edit
341+
lands on its one stored definition, and the record follows it.
340342
- **The API name is immutable after creation.** It is the definition's metadata
341343
identity, so the create form accepts it but the edit form locks it, and a
342344
rename through the data door is rejected. Clone the set to a new name instead.
@@ -348,8 +350,11 @@ has three author-visible consequences:
348350
packaged set entirely.
349351

350352
A cross-package job function needs no hand-authored cross-package set: bind
351-
each package's own sets to one **position** (the union model adds), and use an
352-
overlay where a packaged set must be narrowed.
353+
each package's own sets to one **position** (the union model adds). Where a
354+
packaged set grants too much, clone it, narrow the clone, and bind the clone
355+
in its place: restriction is done by not granting. Narrowing the packaged set
356+
itself is the package author's change: they edit the declaration in the
357+
package and re-publish it (ADR-0086 two-doors).
353358

354359
### Declared ≠ enforced — diagnosing a frozen package set
355360

0 commit comments

Comments
 (0)