Skip to content

Commit ecb7391

Browse files
committed
wip(spec): retire the authored PermissionSetSchema.managedBy
Claude-Session: https://claude.ai/code/session_01D3Ur6XgfkkaDUCCTkviXAr Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6d1660d commit ecb7391

8 files changed

Lines changed: 167 additions & 25 deletions

File tree

‎packages/spec/liveness/permission.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
},
3131
"managedBy": {
3232
"status": "dead",
33-
"verifiedAt": "2026-10-11",
34-
"note": "ADR-0086 D3 — per-record provenance on the metadata-persistence axis (package | platform | user). VERDICT CORRECTED 2026-10-11 (#15204 stage 6b-2): no code reads the AUTHORED key. The cited consumer, bootstrapDeclaredPermissions, stamped `managed_by: 'package'` on every row it wrote and never read `ps.managedBy`, so this row's `live` verdict pointed at a writer of the COLUMN, not a reader of the key; that seeder is now retired (ADR-0131 D3: the boot writes no catalog row). The sys_permission_set.managed_by column is unaffected. Retirement of the authored key is an enforce-or-remove item for the spec seat."
33+
"verifiedAt": "2026-10-16",
34+
"note": "REMOVED 2026-10-16 — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from stored rows and built artifacts by the protocol-18 conversion permission-set-managed-by-removed. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); a set's provenance is the package that ships it, decided from that package's artifact and recorded as packageId. History: ADR-0086 D3 per-record provenance (package | platform | user); VERDICT CORRECTED 2026-10-11 (#15204 stage 6b-2): no code read the AUTHORED key — bootstrapDeclaredPermissions stamped the sys_permission_set.managed_by COLUMN and never read ps.managedBy, and that seeder is retired (ADR-0131 D3). The sys_permission_set.managed_by column is a different surface and is unaffected."
3535
},
3636
"adminScope": {
3737
"status": "live",

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6874,6 +6874,76 @@ const fieldMalformedScalePrecisionRemoved: MetadataConversion = {
68746874
},
68756875
};
68766876

6877+
/**
6878+
* Permission-set `managedBy` removed (protocol 18, #23891 — ADR-0049
6879+
* enforce-or-remove; the liveness ledger's `dead` verdict of 2026-10-11).
6880+
*
6881+
* The key was the AUTHORED per-record provenance of ADR-0086 D3
6882+
* (`package | platform | user`), and nothing ever read it: the retired
6883+
* `bootstrapDeclaredPermissions` seeder stamped the `sys_permission_set.managed_by`
6884+
* COLUMN and never read `ps.managedBy`, and the packaged-set lock decides
6885+
* provenance from the package's artifact. Setup's form row nevertheless
6886+
* promised that package upgrades update a `package` set and leave a
6887+
* `platform` / `user` set alone. The tombstone on `PermissionSetSchema`
6888+
* refuses it at parse (`PERMISSION_SET_MANAGED_BY_REMOVED` in
6889+
* `security/permission.zod.ts`). The column is a different surface and is
6890+
* untouched.
6891+
*
6892+
* ## Why a D2 strip
6893+
*
6894+
* Setup's form row wrote the key into stored `permission` rows, and a 17.x
6895+
* build carried whatever an author wrote into its artifact. After the
6896+
* tombstone the boot door and the metadata save door refuse such a set, and
6897+
* only the D2 table is replayed at the rehydration seams
6898+
* (`applyArtifactForwardConversions`, `applyConversionsToStoredItem`), so a
6899+
* stored row or an installed package's artifact loads only through this
6900+
* entry. The key leaves whatever it holds: the tombstone refuses every value,
6901+
* so a strip that kept one would leave a set that cannot load. The strip is
6902+
* lossless — no grant, owner or upgrade read the value; what an admin believed
6903+
* it did is the D3 record, the semantic entry `permission-set-managed-by-retired`.
6904+
* The emitted path names the set, as
6905+
* `permission-admin-scope-binding-authoring-removed` does.
6906+
*/
6907+
const permissionSetManagedByRemoved: MetadataConversion = {
6908+
id: 'permission-set-managed-by-removed',
6909+
toMajor: 18,
6910+
retiredFromLoadPath: true,
6911+
retiredAfter: '17.7.0',
6912+
surface: 'permission.managedBy',
6913+
summary:
6914+
"permission-set key 'managedBy' removed (ADR-0049 enforce-or-remove — nothing ever read the authored "
6915+
+ "provenance: a set's provenance is the package that ships it, decided from that package's artifact and "
6916+
+ 'recorded as packageId; the sys_permission_set.managed_by column is unaffected. Delete the key)',
6917+
apply(stack, emit) {
6918+
return mapCollection(stack, 'permissions', (ps, path) => {
6919+
// Name the set, not just its index: the notice is the only record an
6920+
// upgrading author gets of WHICH set lost the key.
6921+
const where = typeof ps.name === 'string' ? `${path}(${ps.name})` : path;
6922+
return stripKeys(ps, ['managedBy'], emit, where);
6923+
});
6924+
},
6925+
fixture: {
6926+
before: {
6927+
permissions: [
6928+
// What Setup's form row wrote into a stored row.
6929+
{ name: 'crm_sales_rep', label: 'Sales Rep', packageId: 'crm', managedBy: 'package' },
6930+
{ name: 'env_reviewer', label: 'Env Reviewer', managedBy: 'user' },
6931+
// Already canonical: rides through untouched, by reference.
6932+
{ name: 'env_reader', label: 'Env Reader' },
6933+
],
6934+
},
6935+
after: {
6936+
permissions: [
6937+
{ name: 'crm_sales_rep', label: 'Sales Rep', packageId: 'crm' },
6938+
{ name: 'env_reviewer', label: 'Env Reviewer' },
6939+
{ name: 'env_reader', label: 'Env Reader' },
6940+
],
6941+
},
6942+
// One per set carrying the key.
6943+
expectedNotices: 2,
6944+
},
6945+
};
6946+
68776947
/**
68786948
* `record:chatter` / `record:discussion` `position` converges on the
68796949
* renderer's vocabulary (protocol 18, #8762 — maintainer ruling 2026-08-15:
@@ -16836,6 +16906,7 @@ const MAJOR_18_CONVERSIONS: readonly OrderedConversion[] = [
1683616906
{ conversion: permissionAdminScopeBindingAuthoringRemoved, order: 65 },
1683716907
{ conversion: permissionAllowRestorePurgeRemoved, order: 16 },
1683816908
{ conversion: permissionRlsTagsRemoved, order: 42 },
16909+
{ conversion: permissionSetManagedByRemoved, order: 67 },
1683916910
{ conversion: recordChatterPositionVocabulary, order: 2 },
1684016911
{ conversion: recordHighlightsFieldIconRemoved, order: 10 },
1684116912
{ conversion: reportJoinedChartRemoved, order: 38 },
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// #23891 (ADR-0049 enforce-or-remove; the liveness ledger's `dead` verdict of
4+
// 2026-10-11). `PermissionSet.managedBy` — the authored per-record provenance
5+
// `package | platform | user` of ADR-0086 D3 — was never read: the retired
6+
// `bootstrapDeclaredPermissions` seeder stamped the `sys_permission_set.managed_by`
7+
// COLUMN without reading the key, and the packaged-set lock decides provenance
8+
// from the package's artifact. Setup's form row and its labels promised upgrade
9+
// behaviour nothing kept. The set shape is `strictObject`, so the route is the
10+
// `retiredKey()` tombstone: the key stays in the walked shape as `[RETIRED]`,
11+
// and authoring it is a tsc error and a parse error carrying the prescription.
12+
// The column is a different surface and is untouched. D2:
13+
// `permission-set-managed-by-removed`; D3: `permission-set-managed-by-retired`.
14+
export const entry = 'security/PermissionSet:managedBy';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// #23891 (ADR-0049 enforce-or-remove) — the D3 entry of the
6+
// `permission-set-managed-by-removed` family (one D3 entry per retirement
7+
// family, even when D2 is lossless). Registered key:
8+
// `security/PermissionSet:managedBy`. The strip changes no access decision and
9+
// no upgrade; what it leaves is whatever an admin or author believed the value
10+
// did.
11+
export const entry: SemanticMigration = {
12+
id: 'permission-set-managed-by-retired',
13+
// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
14+
// span AND a table cell.
15+
surface: 'permissions[].managedBy — the authored per-record provenance (package / platform / user) on a '
16+
+ 'permission set',
17+
replacement: 'Nothing: delete the key. A set\'s provenance is the package that ships it, decided from that '
18+
+ 'package\'s artifact and recorded as packageId; a set created in this environment carries neither. The '
19+
+ 'sys_permission_set.managed_by column the platform keeps is a different surface and is unchanged.',
20+
reason: 'The D2 conversion `permission-set-managed-by-removed` deletes `managedBy` from every permission set '
21+
+ 'in author sources, stored permission rows and built artifacts, and the delete is lossless: no seeder, '
22+
+ 'package upgrade, lock or gate ever read the authored key, so no set changes who owns it, what an '
23+
+ 'upgrade does to it, or what it grants. The judgment is about what people believed. Setup\'s form offered '
24+
+ 'Package (upgrade-owned metadata), Platform and User (environment config), and its help text said a '
25+
+ 'Package set is updated by package upgrades while a Platform or User set is left alone. Neither was '
26+
+ 'true: an upgrade updates the sets its package ships, whatever the value; a set marked Package that no '
27+
+ 'package ships was never updated; and a package-shipped set marked Platform or User was never shielded. '
28+
+ 'Any runbook or customization plan that relied on the value to keep an upgrade off a set needs another '
29+
+ 'path — for a packaged set, clone it and edit the clone.',
30+
acceptanceCriteria: 'No authored or stored permission set carries `managedBy`; the parse refuses the key '
31+
+ 'with the prescription. Every set\'s grants are unchanged. Every customization that relied on '
32+
+ 'managedBy to survive a package upgrade lives on a clone of the packaged set, not on the set itself.',
33+
};

‎packages/spec/src/migrations/registry.ts.template‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,20 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [
25922592
+ 'at parse rather than rewritten. Its D3 record is the semantic entry '
25932593
+ '`permission-rls-tags-retired`.',
25942594
},
2595+
{
2596+
id: 'permission-set-managed-by-retired',
2597+
order: 99,
2598+
text:
2599+
'It also retires the permission set\'s authored `managedBy` (ADR-0049 enforce-or-remove; ADR-0086 D3): '
2600+
+ 'the per-record provenance `package` / `platform` / `user`, which nothing ever read — the retired '
2601+
+ 'seeder stamped the `sys_permission_set.managed_by` column without reading the key, and the '
2602+
+ 'packaged-set lock decides provenance from the package\'s artifact — while Setup\'s form promised '
2603+
+ 'that package upgrades update a `package` set and leave the others alone. It is a `retiredKey()` '
2604+
+ 'tombstone on `PermissionSetSchema`, its form row leaves Setup, and the D2 conversion '
2605+
+ '`permission-set-managed-by-removed` strips it from authored sets, stored rows and built artifacts, '
2606+
+ 'retired from the load path. The column is a different surface and is unchanged. Its D3 record is '
2607+
+ 'the semantic entry `permission-set-managed-by-retired`.',
2608+
},
25952609
{
25962610
id: 'platform-global-object-organization-column-retired',
25972611
order: 89,

‎packages/spec/src/security/permission.form.ts‎

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,21 @@ export const permissionForm = defineForm({
3434
{ field: 'label', colSpan: 1, helpText: 'Display label for admins' },
3535
// #19331 — four declared keys had no control, so the only door to any of
3636
// them was the Source tab's free-text JSON. `description` is persisted on
37-
// sys_permission_set and shown in Setup; the other three are the ADR-0086
38-
// D3 provenance pair plus the ADR-0090 D5 baseline flag. The help texts
39-
// are an admin's words; the decision records stay here:
37+
// sys_permission_set and shown in Setup; of the other three, `packageId`
38+
// (ADR-0086 D3) and the ADR-0090 D5 baseline flag keep their rows. The
39+
// help texts are an admin's words; the decision records stay here:
4040
// - `isDefault` (ADR-0090 D5): an app-level set is auto-bound to the
4141
// built-in `everyone` position at boot, after the high-privilege check;
4242
// a package-level set materializes a `sys_audience_binding_suggestion`
4343
// an admin confirms in Setup.
44-
// - `managedBy` (ADR-0086 D3): `package` rows are re-seeded on upgrade;
45-
// `platform` / `user` rows are environment config package seeding
46-
// never touches.
44+
// - `managedBy` left this form with its key (#23891, ADR-0049
45+
// enforce-or-remove): nothing read the authored value, so its labels
46+
// promised upgrade behaviour no code kept.
4747
// - `packageId` (ADR-0086 D3): a value that disagrees with the package
4848
// the set ships in or is saved into is refused
4949
// (`judgePermissionSetOwnerBinding`).
5050
{ field: 'description', type: 'textarea', colSpan: 2, helpText: 'Human-readable description shown in Setup.' },
5151
{ field: 'isDefault', type: 'boolean', colSpan: 1, helpText: 'Gives this set to everyone who signs in, through the built-in everyone position. A set the app itself declares is granted at startup; a set from an installed package becomes a suggestion an admin confirms in Setup. A set with high-privilege permissions is never granted this way. Default false.' },
52-
{ field: 'managedBy', type: 'select', colSpan: 1, helpText: 'Who owns this set: Package means it ships with a package and package upgrades update it; Platform and User mean it was created in this environment and upgrades leave it alone.', options: [
53-
{ label: 'Package (upgrade-owned metadata)', value: 'package' },
54-
{ label: 'Platform (environment config)', value: 'platform' },
55-
{ label: 'User (environment config)', value: 'user' },
56-
] },
5752
{ field: 'packageId', type: 'text', colSpan: 2, helpText: 'The package that ships this set; a value naming any other package is refused. Leave empty for a set created in this environment.' },
5853
],
5954
},

‎packages/spec/src/security/permission.zod.ts‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,18 @@ const ADMIN_SCOPE_AUTHOR_ENVIRONMENT_SETS_REMOVED =
668668
+ '`includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. '
669669
+ 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.';
670670

671+
/**
672+
* [#23891] The prescription for the retired authored key
673+
* `PermissionSetSchema.managedBy` — see the tombstone on that schema.
674+
*/
675+
const PERMISSION_SET_MANAGED_BY_REMOVED =
676+
'`permission.managedBy` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0086 D3) — '
677+
+ 'nothing ever read the authored key: no seeder, package upgrade or lock consulted it, so `package` never made '
678+
+ 'a set upgrade-owned and `platform` or `user` never shielded one from an upgrade. Delete the key; a set\'s '
679+
+ 'provenance is the package that ships it, decided from that package\'s artifact and recorded as `packageId`, '
680+
+ 'and a set created in this environment carries neither. '
681+
+ 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.';
682+
671683
/**
672684
* [ADR-0090 D12] Delegated-administration scope.
673685
*
@@ -885,8 +897,7 @@ export const PermissionSetSchema = lazySchema(() => strictObject(
885897
/**
886898
* [ADR-0086 D3] Owning package for a package-shipped permission set
887899
* (absent ⇒ environment-authored). Persisted on `sys_permission_set`
888-
* records together with the per-record `managedBy` provenance, this is
889-
* what makes the metadata↔config boundary machine-checkable and package
900+
* records, this is what makes the metadata↔config boundary machine-checkable and package
890901
* uninstall well-defined (remove the package's own sets).
891902
*
892903
* The owner is the package that ships the set or that it is saved into:
@@ -903,16 +914,19 @@ export const PermissionSetSchema = lazySchema(() => strictObject(
903914
packageId: z.string().optional().describe('[ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored); a value that disagrees with the package that ships the set, or that it is saved into, is refused by the stack schema (defineStack, os validate, os build), the package registration, and the metadata save, restore and publish doors'),
904915

905916
/**
906-
* [ADR-0086 D3] Per-record provenance on the existing
907-
* metadata-persistence axis (`package | platform | user`):
908-
* `package` = versioned package metadata (seeded by
909-
* `bootstrapDeclaredPermissions`, re-seeded on upgrade, read-mostly for
910-
* admins per ADR-0010); `platform`/`user` = environment config, live-edited
911-
* and never touched by package seeding. Distinct from the `sys_permission_set`
912-
* TABLE's object-affordance `managedBy: 'config'`, which stays.
917+
* REMOVED (#23891, ADR-0049 enforce-or-remove): the authored per-record
918+
* provenance `package | platform | user` (ADR-0086 D3). No code ever read
919+
* the AUTHORED key — the retired `bootstrapDeclaredPermissions` seeder
920+
* stamped the `sys_permission_set.managed_by` COLUMN and never read
921+
* `ps.managedBy`, and the packaged-set lock decides provenance from the
922+
* package's artifact — so the key promised upgrade behaviour nothing kept.
923+
* The column is a different surface and is untouched, as is the
924+
* `sys_permission_set` TABLE's object-affordance `managedBy: 'config'`.
925+
* The ADR-0087 D2 conversion `permission-set-managed-by-removed` strips the
926+
* key from stored rows and built artifacts; the D3 record is the semantic
927+
* entry `permission-set-managed-by-retired`.
913928
*/
914-
managedBy: z.enum(['package', 'platform', 'user']).optional()
915-
.describe('[ADR-0086 D3] Record provenance: package (upgrade-owned metadata) vs platform/user (env config)'),
929+
managedBy: retiredKey(PERMISSION_SET_MANAGED_BY_REMOVED),
916930

917931
/**
918932
* [ADR-0090 D5] Marks this set as the app's baseline for the built-in

‎packages/spec/src/shared/retired-key-removal-version.pin.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import {
8686
} from '../automation/builtin-node-config.zod';
8787
import { NotifyConfigSchema } from '../automation/io-node-config.zod';
8888
import { ScriptConfigSchema, SubflowConfigSchema } from '../automation/schemaless-node-config.zod';
89-
import { AdminScopeSchema } from '../security/permission.zod';
89+
import { AdminScopeSchema, PermissionSetSchema } from '../security/permission.zod';
9090
import { SharingLevel } from '../security/sharing.zod';
9191
import { DatasourceSchema } from '../data/datasource.zod';
9292
import { PageComponentSchema, PageSchema } from '../ui/page.zod';
@@ -247,6 +247,7 @@ const JOIN: Readonly<Record<string, readonly Renderer[]>> = {
247247
tombstone(AdminScopeSchema, 'permission.adminScope', 'manageBindings'),
248248
tombstone(AdminScopeSchema, 'permission.adminScope', 'authorEnvironmentSets'),
249249
],
250+
'permission-set-managed-by-removed': [tombstone(PermissionSetSchema, 'permission', 'managedBy')],
250251
'manifest-permissions-string-list-removed': [
251252
refusal('manifest.permissions as a list of strings', () => ManifestPermissionsSchema.safeParse(['system.user.read'])),
252253
],

0 commit comments

Comments
 (0)