Skip to content

Commit 66949b7

Browse files
feat(plugin-security)!: retire the four security-catalog row objects (#15204 stage 8) (#23222)
Part of #15204 Clause-②: no (narrowing): it retires four declared objects with their data and REST routes, Setup object pages and exports. It adds no export and widens no accept set; the one widening of the early build (a new metadata-protocol export) is D's landed captureUninstallDeclared now. C3 stage 8, the code half (S8), card #23056. It lands after S8-T (#22962, the test half) and D (#22900). #15204 stays open: 7a/7b (dead modules, #22975), 8-post (residue), C7b (#22937 / #15211, the id-to-name rewrite and the column drop) and the docs sweep follow. ## What this changes The four security-catalog row objects (`sys_position`, `sys_permission_set`, `sys_position_permission_set`, `sys_capability`) are no longer registered (ADR-0131 D3). Their data and REST routes go, and so do their Setup object pages. Tables and rows stay for the organization-ownership migration (ADR-0131 D10). - **E1 (ruling 6110777865, E1 → A):** the `permission` mutation projector and its reconcile, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered. A metadata-door save reaches the catalog through the door's own registry write-through. S8-T's `permission-metadata-door-reaches-catalog.dogfood.test.ts` pins this. The modules stay for 7a/7b to delete. - **The walled catalog data-door refusal (ADR-0131 D3)** is unregistered with the write-throughs. It landed after this stage was first built and guards only the three retired objects. - On a `group` or `isolated` deployment, a data-door write on those names now answers `404 OBJECT_NOT_FOUND` (`protocol.assertObjectRegistered`) for every caller. S8-T's `wall-catalog-refusal` dogfood pins this. - The walled refusal at `/meta`, at the clone door and at the `/packages` doors is unchanged. - `walled-catalog-data-door.test.ts` pinned the registration and is deleted with it. The module `walled-catalog-data-door.ts` stays, unwired (no longer registered), for 7a/7b to delete. - **E3 (E3 → A):** `sys_user_permission_set.permission_set_id` is now `Field.text` (maxLength 255) and keeps B2's `hidden`. The column, its ids and both indexes stay the same. - **E6 (seat amendment 6116546399):** the first-admin promotion reads no catalog row. An unscoped grant that names no set and carries a legacy id still withholds the promotion. Both pins hold: S8-T's fresh-boot golden and `grant-readers-unnamed-grant`. - **Uninstall cleanup, on D's landed form:** `cleanupPackagePermissions` keeps D's signature (`declared: { permission }`, captured by `captureUninstallDeclared` in both uninstall doors) and drops the row legs: the catalog-row read, the position-binding delete and the by-id grant leg. It revokes by the declared names alone. The by-id leg is C7b's. - **Carried items:** - Setup nav entries open `/apps/setup/metadata/TYPE?scope=environment`. - These drop the four names: the grants-cache watched set, plugin-sharing `bypassObjects`, the organization_admin and showcase object-permission entries, `SystemObjectName.POSITION` / `PERMISSION_SET`, the platform-object name lists and the tenancy entry. - The lint `capability-reference-unknown` seed-row source goes. - Refusal prose names the clone door instead of the `sys_permission_set` data route. - Settings and spec help text are updated, and `isRowActive` is deleted. - The published plugin-security README stops naming the retired objects. - **Regenerated:** - plugin-security translations and the platform-objects form bundles; - spec reference docs; - the tenancy and tenant-audit censuses; - the keyed-text-bounds `MEASURED` record. - The platform-checklist item `access-security.packaged-permission-set-lifecycle` is retired. - The ADR-0087 semantic entry is `catalog-row-objects-retired`. ## Reconciliation with D (#22900, `c8a9783eca`) This branch was first built on an early D (`fcaedfdaa1`). The `main` merge resolves it as main plus this stage's own commit (`dbae752f6e` against its first parent), replayed three-way, so no line of the early D that the landed D dropped survives. - D's `captureUninstallDeclared` / `UninstallDeclared { permission }` supersedes this stage's earlier `declaredCatalogItemsOf` / `UninstalledCatalogItem`. As a result this diff does not touch `packages/metadata-protocol/src/protocol.ts` or its index, nor cloud-connection's install-local door or vitest config. - The cleanup registration in `security-plugin.ts` is D's. The `resolveCbpRelation` region is untouched. ## Verification Branch head `016d329a36`: main `150da61aa5` (S8-T #22962 landed) plus this diff, 70 files, +325 / −3275. All heavy runs went through `os-verify-lock`; times are shared-box readings. - **Whole repo:** `turbo run build` (74 of 74) and `turbo run typecheck` (147 of 147) are green. `pnpm --filter @objectstack/spec check:generated` reports all 14 artifacts current. The tenant-audit and platform-object tenancy censuses and `check-i18n-bundles` are in sync. - **Suites on the final head:** - plugin-security: 186 files, 3833 tests; - lint: 138 files, 6665 tests; - metadata-protocol: 233 files, 28284 tests; - spec: 662 files, 20114 tests; - dogfood, focused: `anonymous-door-sweep`, `permission-metadata-door-reaches-catalog` and `wall-catalog-refusal`, 3 files, 41 tests. - **Earlier heads of this branch.** Every one already carried this stage and S8-T's tests; each differs from the final head only by later `main` commits and S8-T's own test-file revisions. - `e3d00e12ee` (S8-T `9d9bdd96b2` merged): - service-settings: 43 files, 788 tests; - plugin-approvals: 73 files, 1035 tests; - plugin-hono-server: 29 files, 336 tests; - cloud-connection: 44 files, 540 tests; - verify: 38 files, 301 tests; - client: 51 files, 653 tests; - organizations: 13 files, 160 tests; - app-crm: 6 files, 46 tests; - app-showcase: 33 files, 409 tests; - 4 focused plugin-security files (builtin-positions, the fresh-boot golden, grant-readers-unnamed-grant, cleanup-package-permissions), 71 tests; - the 2 plugin-auth grant-reader files, 9 tests. - `857bdb7ecc`: - dogfood, full: 253 files plus 1 skipped, 2079 tests; - plugin-auth: 136 files, 2780 tests. - `34f4f777c4`: cli unit, 285 files, 4283 tests. - `31933c0ccf`: - core: 95 files, 2397 tests; - platform-objects: 70 files, 1091 tests; - plugin-sharing: 41 files, 1007 tests; - objectql: 409 files, 7998 tests; - rest: 275 files, 5761 tests; - runtime: 363 files, 5174 tests. - **Premise (6110777865):** a metadata-door `permission` save reaches the catalog with the projector unwired. `permission-metadata-door-reaches-catalog.dogfood.test.ts` passes its precondition and its create, edit and delete cases on the combined tree, and no live caller of the projector, either write-through, the backfill or the walled door remains. - **Gates:** the list derived three-dot against `origin/main` (143 families), unioned with the dispatch's lead list, ran on `016d329a36`. That is 145 commands, with `check:pm-dispatch-gates` run detached, and `dispatch-gates --ran` reconciles 143 of 143. 144 exit 0. - The one exit 1 is `check:platform-checklist`: 8 `ABSENT SYMBOL` / anchor-floor findings in `access-security.json`, `attachments-storage.json` and `search.json`. - All 8 are identical on `main` `150da61aa5` (`node scripts/check-platform-checklist.mjs`, compared line for line). The gate is kept out of per-PR CI. - **Not run locally:** `pnpm lint` (the repo-wide scan is CI's). ## Acceptance notes - Out of scope here, and carried elsewhere: - 7a/7b (#22975): the unwired `permission-set-projection.ts`, `position-write-through.ts`, `position-environment-backfill.ts`, `walled-catalog-data-door.ts` (and its ADR anchor) and the `writesExecutedByDataDoor` WeakSet. They are no longer registered, but `catalog-reference-report.ts`, `packaged-permission-set-overlay-detection.ts` and `position-environment-backfill.ts` still import helpers from them, so 7a/7b re-home `POSITION_OBJECT`, `metadataDoorAcceptsPositionName`, `tryFind` and `ProjectionLogger` first. - 8-post: comment and description residue, the per-object gates in `security-plugin.ts` and `delegated-admin-gate.ts`, and `SYSTEM_ROW_PROVENANCE`. - C7b: the CLI organization-ownership inventory, `auto-org-admin-grant` `resolvePermissionSetIdsForName`, the grant-name backfill and the by-id uninstall leg. - The docs sweep: the hand-written `content/docs` pages and ADRs that name the objects, including `system-context.mdx` row 55b's data-door clause. - `18.0.0-next.0`'s CHANGELOG entry for the walled refusal names a data-door `403` on the three objects. That is true of `next.0`, and this diff's changeset records the move to `404` for the next release. The ADR-0087 entry `walled-catalog-tenant-writes-retired` promised that data-door `403` in its `surface` and `acceptanceCriteria` for the v18 upgrade guide. This diff corrects both clauses (`d9f53a69e5`), so the guide states the `404` after stage 8. - `lint/src/validate-capability-references.seed-row.test.ts` pins new behaviour that cannot pass on `main` before this lands, so it rides here rather than in S8-T. - The objectui pin `1fb1ced362` has the `metadata/:type` catalog route with `?scope=environment`. It imports none of the removed exports (`SystemObjectName.POSITION` / `PERMISSION_SET`, `SysPositionDetailPage`, `isRowActive`); its `sys_permission_set` string checks become dead branches, not build breaks. ## 维护者速读(草稿) **改了什么** - 岗位、权限集、权限集-岗位绑定、能力这四张“目录行对象”不再注册。对应的数据接口、REST 路由和 Setup 里的对象页面随之下线。数据表和行都保留,留给之后的组织归属迁移处理。 - 权限集保存后不再写行镜像,由元数据门直接写入安全目录。两个数据门回写、一次性岗位补数,以及多租户下数据门那一半的目录写入拒绝都已摘除。 - 首位平台管理员的提升不再读取目录行。 - 卸载包时只按包声明的权限集名称撤销授权,不再读写目录行。 **为什么改** - ADR-0131 D3 规定岗位、权限集、能力只有一个归宿,就是安全目录(环境注册表)。行对象只是镜像,没有任何授权逻辑读取它,留着只会形成“声明了但不兑现”的第二套入口,AI 和人都容易写错地方。 - 这是 C3 第 8 阶段,前置的 D(#22900)和测试半边 S8-T(#22962)先落地。 **风险与代价(含回滚)** - 破坏性变更:直接调用 `/api/v1/data/sys_permission_set` 等四个对象接口的客户端会收到 404。changeset 写明了 FROM → TO 迁移:改用 `/api/v1/meta/permission|position|capability` 和克隆接口。 - 多租户隔离部署下,目录写入的拒绝仍在 `/meta`、克隆门和包门生效。数据门那一半随对象一起消失,统一答 404。 - 不删表、不删数据:改的是代码、测试、生成物、一份 v18 迁移条目,以及 ADR-0086 的一行锚点。回滚就是 revert 本 PR,无数据迁移需要回退。 - 规模:70 余个文件,删除为主(对象定义、生成的翻译,以及随门一起删除的一份测试),超过 3000 行;另外改了 ADR-0086 的一行锚点,`docs/adr/**` 属受管路径。按规模和路径都属 Tier H。 **席位意见** **你要做的** - 本 PR 超过 3000 行,且碰了受管路径 `docs/adr/**`,需要你批准后由席位落地。批准前席位会附上合约复核结论。 --- _Generated by [Claude Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 99e944d commit 66949b7

75 files changed

Lines changed: 456 additions & 3352 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
'@objectstack/platform-objects': minor
4+
'@objectstack/spec': minor
5+
'@objectstack/core': minor
6+
'@objectstack/metadata-protocol': patch
7+
'@objectstack/objectql': minor
8+
'@objectstack/plugin-sharing': patch
9+
'@objectstack/lint': patch
10+
'@objectstack/service-settings': patch
11+
---
12+
13+
feat(plugin-security)!: the four security-catalog row objects are retired — `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` (ADR-0131 D3)
14+
15+
Clause-②: no (narrowing)
16+
17+
<!-- adr-0087: registered catalog-row-objects-retired -->
18+
19+
**BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes.
20+
21+
Positions, permission sets and capabilities live in the security catalog — the environment registry, fed by package declarations and by environment metadata saved through `/api/v1/meta` (ADR-0131 D3). The four row objects that mirrored them are no longer registered, so their data and REST routes and their Setup object pages are gone. No table is dropped and no row is deleted.
22+
23+
FROM → TO:
24+
25+
- `GET|POST|PATCH|DELETE /api/v1/data/sys_permission_set` → `GET|PUT|DELETE /api/v1/meta/permission/:name`; to copy a set, `POST /api/v1/security/permission-sets/:name/clone` with `{ name, label }`.
26+
- `… /api/v1/data/sys_position` → `/api/v1/meta/position/:name`. A position's permission sets are its definition's `permissionSets`.
27+
- `… /api/v1/data/sys_position_permission_set` → the position definition's `permissionSets` (`PUT /api/v1/meta/position/:name`).
28+
- `… /api/v1/data/sys_capability` → read-only `GET /api/v1/meta/capability`; a package declares a capability with `defineCapability` (`stack.capabilities`).
29+
- Setup → Access Control → Positions / Capabilities / Permission Sets now open Setup's catalog lists (`/apps/setup/metadata/<type>?scope=environment`) instead of the object pages; the `sys_position` record page (`SysPositionDetailPage`) is removed.
30+
- An object permission on one of the four names in a permission set grants nothing and can be deleted.
31+
32+
What else changes:
33+
34+
- `sys_user_permission_set.permission_set_id` is a plain text column now, not a lookup: same column, same stored ids, same indexes. It is not written and grants nothing; a grant names its set by `permission_set`.
35+
- A permission set saved through the metadata door is written to no row: the `permission` mutation projector, the `sys_permission_set` and `sys_position` data-door write-throughs and the row-only position backfill are no longer registered.
36+
- On a fresh `single`-posture database the first human is promoted to platform administrator without reading any catalog row. An unscoped grant written before `sys_user_permission_set.permission_set` existed (it names no set) still withholds the promotion until the organization-ownership migration names it.
37+
- Uninstalling a package still revokes every grant naming a permission set the package declared, by the names the uninstalling door captures before the withdrawal (`declared`, `captureUninstallDeclared`), and now by those names alone: the cleanup reads and removes no `sys_permission_set` or `sys_position_permission_set` row, so the `sets` and `positionBindings` counts on its outcome are always 0. A grant written before `sys_user_permission_set.permission_set` existed names no set and is not reached; the organization-ownership migration names it.
38+
- On a `group` or `isolated` deployment the data-door half of the walled catalog refusal goes with the objects: a data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability` answers `404 OBJECT_NOT_FOUND` for every caller instead of `403 PERMISSION_DENIED` below the platform operator. The refusal at the `/meta` write doors, the permission-set clone door and the `/packages` write doors is unchanged.
39+
- `capability-reference-unknown` no longer counts a `sys_capability` seed row as a declared capability — no runtime reader consults one. Declare the capability with `defineCapability`.
40+
- On an existing database `os migrate plan`'s unmanaged-table sweep now lists the `sys_position`, `sys_permission_set`, `sys_position_permission_set` and `sys_capability` tables: they keep their rows and no object declares them any more. The sweep only reports; nothing is dropped, and the tables stay for the organization-ownership migration.
41+
42+
Removed exports, no replacement:
43+
44+
- `@objectstack/plugin-security`: the object definitions `SysPosition`, `SysPermissionSet`, `SysPositionPermissionSet`, `SysCapability` (from `./objects`); `securityObjects` lists the remaining three.
45+
- `@objectstack/platform-objects/pages`: `SysPositionDetailPage`.
46+
- `@objectstack/spec`: `SystemObjectName.POSITION`, `SystemObjectName.PERMISSION_SET`; the four names leave `PLATFORM_OBJECTS_BY_PACKAGE['plugin-security']` and `PLATFORM_PROVIDED_OBJECT_NAMES`.
47+
- `@objectstack/core`: `isRowActive` and `ActivatableRow` — a catalog row's `active` column decides nothing; deactivation is the activation ledger's (`sys_metadata_activation`).
48+
- `@objectstack/objectql`: the published `PLATFORM_OBJECT_TENANCY` record loses its `sys_permission_set` key (the other three names were never in it). A removed key of an exported constant is a narrowing, so `@objectstack/objectql` takes `minor` under the launch-window convention for breaking changes.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ cannot read, and they are neither in nor out.
226226
| write call sites on the application surface | **228** |
227227
| …whose object name is statically decidable | 155 |
228228
| …whose object name is chosen at run time | 73 |
229-
| …against an object with tenancy ENABLED | 154 |
229+
| …against an object with tenancy ENABLED | 151 |
230230
| …against an object that declares tenancy off | 1 |
231231
| threading a tenant context | 167 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
@@ -240,9 +240,9 @@ cannot read, and they are neither in nor out.
240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242242
| receiver carried a readable engine type | 186 |
243-
| receiver erased, placed by the object NAME | 30 |
243+
| receiver erased, placed by the object NAME | 27 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 7 |
245-
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
245+
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 8 |
246246

247247
| object name spelled inline | 103 |
248248
| object name spelled through a `const` | 52 |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-13 at `ae14c39c5`.
300+
Measured on 2026-10-13 at `7c01d30c9`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 654 |
304+
| tracked non-test sources scanned | 650 |
305305
| engine-shaped types recognised | 73 |
306-
| declared objects in the registry | 120 |
306+
| declared objects in the registry | 116 |
307307
| same-named calls subtracted as non-engine | 160 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎content/docs/references/security/permission.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const result = AdminScopeSchema.parse(data);
116116
| :--- | :--- | :--- | :--- |
117117
| **name** | `string` | ✅ | Permission set name, unique per organization (lowercase snake_case) |
118118
| **label** | `string` | optional | Display label |
119-
| **description** | `string` | optional | Human-readable description shown in Setup (persisted as sys_permission_set.description) |
119+
| **description** | `string` | optional | Human-readable description shown in Setup |
120120
| **packageId** | `string` | optional | [ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored) |
121121
| **managedBy** | `Enum<'package' \| 'platform' \| 'user'>` | optional | [ADR-0086 D3] Record provenance: package (upgrade-owned metadata) vs platform/user (env config) |
122122
| **isDefault** | `boolean` | optional (default: `false`) | [ADR-0090 D5] App baseline for the everyone position: app-level sets are auto-bound at boot (guarded, idempotent); package-level sets become install-time suggestions an admin confirms |

‎content/docs/references/system/auth-config.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Advanced / low-level Better-Auth options
5454
| :--- | :--- | :--- | :--- |
5555
| **posture** | `Enum<'invite_only' \| 'email_domain' \| 'open'>` | optional (default: `"invite_only"`) | Who may self-register into this environment: invite_only (default — operator acts only), email_domain (allowlisted email domains), or open (anyone). email_domain forces email verification on; open forces it on unless the deployment explicitly sets emailAndPassword.requireEmailVerification to false. |
5656
| **allowedEmailDomains** | `string[]` | optional | Email domains admitted to self-register under posture email_domain (exact, case-insensitive match; subdomains need their own entries). Required non-empty for email_domain; refused under other postures. |
57-
| **selfRegistrationPermissionSet** | `string` | optional | sys_permission_set name granted to each self-registrant. Required when posture is email_domain or open; refused for invite_only. admin_full_access is refused. |
57+
| **selfRegistrationPermissionSet** | `string` | optional | Name of the permission set granted to each self-registrant. Required when posture is email_domain or open; refused for invite_only. admin_full_access is refused. |
5858

5959

6060
---
@@ -171,7 +171,7 @@ OIDC / Generic OAuth2 provider configuration for enterprise SSO
171171
| :--- | :--- | :--- | :--- |
172172
| **posture** | `Enum<'invite_only' \| 'email_domain' \| 'open'>` | optional (default: `"invite_only"`) | Who may self-register into this environment: invite_only (default — operator acts only), email_domain (allowlisted email domains), or open (anyone). email_domain forces email verification on; open forces it on unless the deployment explicitly sets emailAndPassword.requireEmailVerification to false. |
173173
| **allowedEmailDomains** | `string[]` | optional | Email domains admitted to self-register under posture email_domain (exact, case-insensitive match; subdomains need their own entries). Required non-empty for email_domain; refused under other postures. |
174-
| **selfRegistrationPermissionSet** | `string` | optional | sys_permission_set name granted to each self-registrant. Required when posture is email_domain or open; refused for invite_only. admin_full_access is refused. |
174+
| **selfRegistrationPermissionSet** | `string` | optional | Name of the permission set granted to each self-registrant. Required when posture is email_domain or open; refused for invite_only. admin_full_access is refused. |
175175

176176
### Nested Shape: `AuthConfig.advanced`
177177

‎docs/adr/0086-authz-metadata-config-boundary-and-cross-package-composition.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ flagged inline below.
373373
|---|---|---|
374374
| **D1** classification on the provenance axis | **Adopted**; table vocabulary now stale — see "revised table" below | Principle intact. Rows renamed/removed by ADR-0090 (Profile gone, role→position). |
375375
| **D2** DEFINITION↔ASSIGNMENT principle | **Adopted, reinforced** | ADR-0090 D2 leans on it to justify removing Profile; ADR-0091 dates the *assignment* rows only (`valid_from`/`valid_until`), confirming they are env data. |
376-
| **D3** `packageId` + provenance `managedBy` on the set | **Implemented** | `packages/spec/src/security/permission.zod.ts#managedBy` (`packageId`, tagged `[ADR-0086 D3]`) + (`managedBy` enum); record fields `package_id`/`managed_by`/`customized` + index on `packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts#managedBy`. |
376+
| **D3** `packageId` + provenance `managedBy` on the set | **Implemented** | `packages/spec/src/security/permission.zod.ts#managedBy` (`packageId`, tagged `[ADR-0086 D3]`) + (`managedBy` enum); record fields `package_id`/`managed_by`/`customized`; the `sys_permission_set` row object that carried them is retired by C3 stage 8 (ADR-0131 D3; `packages/spec/src/migrations/entries/semantic/18.catalog-row-objects-retired.ts`). |
377377
| **D4** Shape B (packages ship own sets; union; shared slices env-only) | **Adopted, reinforced; subtract layer upgraded** | ADR-0090 D9 / Alt #6 reject package-written shared/builtin sets for the same reasons. Composition is now anchored at the **position** ("bind several packages' sets to one position"), ADR-0094 D5. **Subtract layer changed** — see ⚠️ below. |
378378
| **D5** `bootstrapDeclaredPermissions` | **Implemented; `isDefault` semantics refined by ADR-0090 D5** | `plugin-security/src/bootstrap-declared-permissions.ts` (invoked at `kernel:ready`, `security-plugin.ts`); seeds `managed_by:'package'` + `package_id`, idempotent/upgrade-aware; closes the ADR-0078 inert-metadata smell the D5 header calls out. `isDefault` no longer means "profile fallback": app-level default sets **auto-bind to the `everyone` position**; package-level default sets materialize a `sys_audience_binding_suggestion` an admin confirms (new object). |
379379
| **D6** package Access door under draft/publish | **Implemented** | objectui writes the package door as `mode:'draft', packageId` (`PermissionMatrixEditor.tsx`); env door stays live and, per ADR-0094 D3, that "live" save is itself redirected into a metadata overlay. |

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3636
| Write call sites | 228 |
3737
| Object name statically decidable | 155 |
3838
| Object name chosen at run time | 73 |
39-
| Against a tenancy-enabled object | 154 |
39+
| Against a tenancy-enabled object | 151 |
4040
| Against an object declaring tenancy off | 1 |
4141
| Threading a tenant context | 167 |
4242
| Provably carrying none | 8 |
@@ -90,13 +90,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-13 at `ae14c39c5`.
93+
Measured on 2026-10-13 at `7c01d30c9`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 654 |
97+
| tracked non-test sources scanned | 650 |
9898
| engine-shaped types recognised | 73 |
99-
| declared objects in the registry | 120 |
99+
| declared objects in the registry | 116 |
100100
| same-named calls subtracted as non-engine | 160 |
101101

102102
## Every site
@@ -163,10 +163,10 @@ Measured on 2026-10-13 at `ae14c39c5`.
163163
| `packages/plugins/plugin-security/src/invitation-placement.ts` | `insert` | `sys_user_position` | enabled | elevated | 1 |
164164
| `packages/plugins/plugin-security/src/permission-set-projection.ts` | `insert` | `object` | undecidable | context, elevation undecidable | 1 |
165165
| `packages/plugins/plugin-security/src/permission-set-projection.ts` | `update` | `object` | undecidable | context, elevation undecidable | 1 |
166-
| `packages/plugins/plugin-security/src/permission-set-projection.ts` | `delete` | `sys_permission_set` | enabled | elevated | 1 |
166+
| `packages/plugins/plugin-security/src/permission-set-projection.ts` | `delete` | `sys_permission_set` | undeclared-name | elevated | 1 |
167167
| `packages/plugins/plugin-security/src/position-environment-backfill.ts` | `insert` | `DATA_MIGRATION_FLAG_OBJECT` | undecidable | elevated | 1 |
168-
| `packages/plugins/plugin-security/src/position-write-through.ts` | `delete` | `sys_position` | enabled | elevated | 1 |
169-
| `packages/plugins/plugin-security/src/position-write-through.ts` | `update` | `sys_position` | enabled | elevated | 1 |
168+
| `packages/plugins/plugin-security/src/position-write-through.ts` | `delete` | `sys_position` | undeclared-name | elevated | 1 |
169+
| `packages/plugins/plugin-security/src/position-write-through.ts` | `update` | `sys_position` | undeclared-name | elevated | 1 |
170170
| `packages/plugins/plugin-security/src/suggested-audience-bindings.ts` | `delete` | `sys_audience_binding_suggestion` | enabled | context, elevation undecidable | 1 |
171171
| `packages/plugins/plugin-security/src/suggested-audience-bindings.ts` | `insert` | `sys_audience_binding_suggestion` | enabled | context, elevation undecidable | 1 |
172172
| `packages/plugins/plugin-security/src/suggested-audience-bindings.ts` | `update` | `sys_audience_binding_suggestion` | enabled | context, elevation undecidable | 3 |

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2560,8 +2560,9 @@
25602560
"id": "access-security.packaged-permission-set-lifecycle",
25612561
"title": "Packaged permission-set lifecycle (#11513) survives ADR-0126 unchanged: locked base at the data door, clone-to-customize with every facet carried, `active` row-state flip — and NONE of it writes a sys_metadata_activation row",
25622562
"since": "v16",
2563-
"status": "active",
2564-
"revision": 1,
2563+
"status": "retired",
2564+
"retiredReason": "C3 stage 8 (ADR-0131 D3) retired the sys_permission_set row object: its data door, its Activate / Deactivate / Clone row actions and its `active` column are gone. A packaged set's base stays locked at the metadata door, a copy is made with POST /api/v1/security/permission-sets/:name/clone, and a set is switched off in the activation ledger (sys_metadata_activation).",
2565+
"revision": 2,
25652566
"priority": "P2",
25662567
"surface": "mixed",
25672568
"personas": [
@@ -2646,7 +2647,7 @@
26462647
"source": [
26472648
"packages/plugins/plugin-security/src/packaged-permission-set-lock.ts (classifier + both refusals; the SchemaRegistry-not-managed_by decision)",
26482649
"packages/plugins/plugin-security/src/permission-set-projection.ts (insert-path lock + duplicate 409; the #4669 row-state carve-out ordered BEFORE the update-path lock)",
2649-
"packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts#bodyExtra (activate/deactivate bodyExtra {active}, clone_permission_set params — the payload contract)",
2650+
"packages/runtime/src/domains/permission-set-clone.ts (the clone door that replaced the retired object's clone_permission_set action)",
26502651
"docs/adr/0126-packaged-metadata-customization-model.md §1.1 (the #11513 machinery, named as the regime's first instance), §8 item 3 (permission sets converge later; the landed shape stays valid meanwhile)",
26512652
"#11513 (lock the base, clone to customize — the 2026-08-24 ruling), #11703 (the three dropped clone facets), #12159 (ruling 2026-08-26: no ledger rows for permission sets pending the convergence card), #12438 (the sweep)"
26522653
],
@@ -2656,6 +2657,12 @@
26562657
"date": "2026-08-26",
26572658
"change": "new — the landed #11513 surface (data-door lock, clone action, active row-state lifecycle) had no checklist item: readonly-package-locks-studio covers the /meta door on objects and permission-matrix-edit-loop explicitly scopes itself to WRITABLE sets, so the packaged-set data-door lifecycle was uncovered end to end. Authored as the ADR-0126 regression guard with the no-ledger-row wall as its ⭐ clause, per the 2026-08-26 ruling in #12159 (card 已知边界 4): flipping active or cloning must keep writing sys_permission_set state only, never a sys_metadata_activation row",
26582659
"ref": "#12438"
2660+
},
2661+
{
2662+
"revision": 2,
2663+
"date": "2026-10-12",
2664+
"change": "retired — the sys_permission_set row object, its data door and its row actions are retired with C3 stage 8 (ADR-0131 D3)",
2665+
"ref": "#15204"
26592666
}
26602667
]
26612668
},

‎docs/qa/platform-checklist/coverage.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@
223223
"access-security.sharing-rule-authoring-ui",
224224
"access-security.owd-save-gate",
225225
"access-security.share-link-capability-tokens",
226-
"access-security.packaged-permission-set-lifecycle",
227226
"access-security.platform-owner-email-anchor",
228227
"access-security.me-permissions-aggregation-parity",
229228
"api-backend.api-methods-verb-gate"

‎examples/app-showcase/access-matrix.json‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -199,26 +199,6 @@
199199
"viewAllRecords": false,
200200
"modifyAllRecords": false
201201
},
202-
{
203-
"permissionSet": "showcase_field_ops_delegate",
204-
"object": "sys_permission_set",
205-
"create": false,
206-
"read": true,
207-
"edit": false,
208-
"delete": false,
209-
"viewAllRecords": false,
210-
"modifyAllRecords": false
211-
},
212-
{
213-
"permissionSet": "showcase_field_ops_delegate",
214-
"object": "sys_position",
215-
"create": false,
216-
"read": true,
217-
"edit": false,
218-
"delete": false,
219-
"viewAllRecords": false,
220-
"modifyAllRecords": false
221-
},
222202
{
223203
"permissionSet": "showcase_field_ops_delegate",
224204
"object": "sys_user",

0 commit comments

Comments
 (0)