You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit be48c5e
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec)!: retire AdminScope.manageBindings and authorEnvironmentSets — delegation rights no door honoured (#23488) (#23601)
Fixes#23488
Clause-②: yes
Executes ruling of record 6181988435 on #23488 (letter A, the
maintainer's 「同意」): `AdminScope.manageBindings` and
`AdminScope.authorEnvironmentSets` retire inside the 18 major by the
`spec-property-retirement` route, the delegated-admin gate's
strict-containment comparison stops reading them, and the text that
described the removed gates is corrected. The changeset declares
`Clause-②: yes (narrowing)`, BREAKING, ADR-0087 disposition
`registered`.
The ADR half (ADR-0090 D12 status line, ADR-0086 line 446) is a separate
Tier H docs PR on branch
`claude/issue-23488-adr-delegation-rights-retired`, as ruling item 5
requires. The objectui half (Studio's two switches, help strings, i18n
keys, pin test) is objectstack-ai/objectui#12406, filed by the seat
(ruling item 4); this PR does not touch objectui.
## What changes
**1. The retirement kit (`@objectstack/spec`).**
- Two `retiredKey()` tombstones on `AdminScopeSchema`
(`security/permission.zod.ts`). Each prescription says what the ruling
binds: delete the key; a delegate's scope is placement, assignment and
the allowlist. Both close with the house `os migrate meta --from 17`
sentence.
- `DelegableAdminScope` (`contracts/security-service.ts`) drops both
fields.
- D2 conversion `permission-admin-scope-binding-authoring-removed` in
`MAJOR_18_CONVERSIONS` (order 65, `retiredFromLoadPath: true`,
`retiredAfter: '17.7.0'`). It strips both keys from every
`permissions[].adminScope`, whatever they hold, one notice per key, and
the notice path names the set. Both keys carried `.default(false)`, so a
17.x parse materialized them in every declared scope. The rehydration
seams replay this entry over stored rows and built artifacts (the
`email_template.isSystem` precedent; `acceptRetiredDefaultResidue` is
not adopted).
- `RETIRED_KEYS_BY_MAJOR[18]`: `security/AdminScope:manageBindings`,
`security/AdminScope:authorEnvironmentSets`.
- D3 semantic entry `permission-admin-scope-binding-authoring-retired`,
which states the containment change, plus its `STEP18_RATIONALE`
fragment (order 97).
- The form's help text, the generated reference page,
`authorable-surface/security.json` (two rows become RETIRED),
`authorable-defaults/security.json` (two default rows go), the liveness
note (`liveness/permission.json`, re-verified, still `live`), and the
removal-version pin's JOIN row.
**2. The containment change (`@objectstack/plugin-security`, ruling item
2).**
- `definitionContainsStrictly` compares `includeSubtree`,
`manageAssignments` and the allowlist only. `resolveHeldScopes` and
`describeDelegableScope` drop both flags.
- Case (ii) of reading 6172654646 becomes a refusal: a delegate whose
own scope carries an inert `true` can no longer hand out a scope
otherwise equal to its own. It answers `403 PERMISSION_DENIED`.
- New pins in `delegated-admin-gate.test.ts`, under the describe
`[#23488] the retired binding/authoring flags count on no containment
axis`:
- case (ii) refused, asserting `code` + `status` + the message's first
clause;
- a CONTROL: the same delegate grants a scope narrower on a real axis;
- case (i) reversed: the granted side's retired `true` no longer
out-reaches the delegate.
- Mechanism assumption 1 holds: the gate fixture set both flags `true`
on both sides (`EAST_SCOPE`), so these pins are new, not flipped
assertions. The six other plugin-security fixtures drop the keys.
**3. The legacy-row ceremony (`@objectstack/core`, ruling item 3) —
measured, and the mapper drops the keys.**
- The measurement used a scratch test, not committed, in
`packages/metadata-protocol` at `43c50c843`. It took
`permissionSetDefinitionFromRow` over a 17.x-shaped row (`admin_scope`
carrying both keys as `false`) and passed the body to
`ObjectStackProtocolImplementation.saveMetaItem`. That is the door `os
migrate organization-ownership` composes
(`packages/cli/src/commands/migrate/organization-ownership.ts:567-578`).
- The door answered
`{"ok":false,"code":"INVALID_METADATA","status":422}` at
`adminScope.manageBindings`, with the tombstone's prescription.
- CONTROL: the same row without the two keys saved,
`{"ok":true,"success":true}`.
- So the save does not replay the conversion. That matches
`conversions/stored.ts`'s header: "The write path must NOT use this:
`saveMetaItem` rejects off-spec bodies with the current schema".
- Fix: `permission-set-row-definition.ts` drops both keys from the
parsed `admin_scope` (`RETIRED_ADMIN_SCOPE_KEYS`).
- New pin: the body without the keys passes `PermissionSetSchema` for
`false`, `true` and a non-boolean value. A CONTROL shows the same body
with the key kept is refused at `['adminScope','manageBindings']`.
- `packages/cli/src/utils/organization-ownership-apply.ts` is read only,
not edited (mechanism assumption 4 holds).
**4. Position 3 text (docs accuracy, no contract change).**
- `content/docs/permissions/delegated-administration.mdx`: the
action-flag and strict-containment rows.
- `content/docs/permissions/permission-sets.mdx`: the strict-containment
sentence.
- The showcase's seven sentences: five in
`examples/app-showcase/src/security/permission-sets.ts` (`:26-27`,
`:166-167`, `:357-362`, `:384-385`, and the two keys deleted) and two in
`examples/app-showcase/src/docs/showcase_tour_security.md`.
- The spec JSDoc (`AdminScopeSchema` header, `assignablePermissionSets`,
`PermissionSet.adminScope`).
- The ledger note.
- The two plugin-security comments: `delegated-admin-gate.ts`
`scopesCoverUser` and the placement comment, and
`objects/sys-user-permission-set.object.ts`.
- `packages/metadata-core/src/meta-write-capability.ts:184-185`: the
security plugin's data door no longer asks the walled verdict. Measured:
`git grep -n 'walledCatalogWriteVerdict(' -- packages` finds no caller
under `packages/plugins`.
- `docs/design/permission-model.md` (the delegated-admin bullets and the
authoring sample).
- `packages/platform-objects`' four `metadata-forms` bundles:
- `en` was regenerated with `pnpm i18n:extract`;
- the three translated `helpText` values (zh-CN, ja-JP, es-ES) were
edited by hand, since AGENTS.md allows translated-locale values to be
hand-written;
- the `adminScope` path has no source-hash entry, so it is
legacy-trusted.
- `packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts` now
asserts both keys are absent from the served definition.
**5. Tree-scoped absence pin.**
`packages/spec/src/security/admin-scope-binding-authoring-retirement.test.ts`
(registered in `vitest.repo-tests.json`) pins:
- the refusal at every door: schema, the `permission` write-door
binding, and `defineStack` with `STACK_SCHEMA_INVALID` / 422;
- the tsc channel (`@ts-expect-error`);
- the D2 strip: stored-row rehydration, the artifact before and after,
idempotence, and that it is retired from the load path;
- the registration;
- an absence walk over the five roots already declared for
`@objectstack/spec` in `scripts/cross-package-test-inputs.mjs`.
## File surface vs the claim
These are outside the claim's list, each with the reason:
- `docs/design/permission-model.md`: the reading (Reading 5) lists it,
and it named both keys as authorable.
- `examples/app-showcase/src/docs/showcase_tour_security.md`: two of
"the showcase's seven" (ruling item 5).
- `packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts`: it
asserted `manageBindings === false`.
- `packages/plugins/plugin-security/src/*.test.ts` fixtures.
- `packages/spec/src/shared/retired-key-removal-version.pin.test.ts`:
its guard requires a JOIN row for every newest-stable retirement.
- `packages/spec/vitest.repo-tests.json`.
- The generated `authorable-*` and reference files.
Mechanism assumption 3 does not hold: no generator wrote under
`skills/**`, so this PR touches no governed surface.
## Verification
**Pre-merge head `0221b165f`.** Each command ran through
`scripts/pm/os-verify-lock.sh`, with the exit code captured before any
pipe:
- `pnpm --filter @objectstack/spec test`: 677 files passed, 1 skipped;
20729 tests passed.
- `pnpm --filter @objectstack/spec test:repo`: 62 files, 1051 tests
passed.
- `pnpm --filter @objectstack/spec typecheck`: exit 0. That includes
`check:test-typecheck`, so the two `@ts-expect-error` lines are compiled
and used.
- `pnpm --filter @objectstack/plugin-security typecheck`: exit 0.
- `pnpm --filter @objectstack/core test`: exit 0.
- `pnpm --filter @objectstack/platform-objects test`: exit 0.
- plugin-security, 7 targeted files (`delegated-admin-gate*.test.ts`,
`granted-by-writer-provenance`, `reserved-identity-names`,
`can-write-object-admission`): 206 passed.
- `pnpm --filter @objectstack/spec check:generated`: one stale artifact
(`content/docs/references/**`), regenerated with `gen:docs`. Every other
gate was green.
**Reverse verification, at `0221b165f`, both restored and proved by blob
hash and an empty `git diff HEAD`:**
- **Gate.** `delegated-admin-gate.ts` at the branch point `fa6a6085c`
was written over the file. Of the four selected cases, 2 failed (case
(ii) and case (i) reversed) and 2 passed (the CONTROL and the existing
equal-scope refusal). That is the expected direction.
- **Mapper.** `scripts/ablation-replace.mjs` removed the
`withoutRetiredAdminScopeKeys(...)` call (anchor 1 → 0, blob
`03f32ba6f610` → `01d09ba3e389`). The mapper pin failed (1 failed, 12
passed), and the file was restored to the HEAD blob.
**objectui at the pin (`b9814b334d`), measured by reproduction, not by a
full objectui build.**
- I typechecked a copy of `PermissionAdvancedFacets.tsx`'s typing
against this branch's built `@objectstack/spec` dist. The copy covered
the `AdminScope` import, `Partial` alias, `setScopeDetail`, the three
switches' `scope[key]` and the `{ [key]: !!v } as Partial` write, plus
`DelegableAdminScope`. It used objectui's compiler options (`strict`,
`moduleResolution: bundler`) and TypeScript 6.0.3, and exited 0.
- CONTROL: a literal `{ businessUnit: 'x', manageBindings: false }`
typed as `AdminScope` fails `TS2322` (the retired mark), and
`DelegableAdminScope['manageBindings']` fails `TS2339`. So the
reproduction reads the new types.
- A full objectui build at the pin is NOT MEASURED.
**Merged head `3854d1403`** (`origin/main` merged through
`scripts/pm/os-regen-merge.sh`): the closure rebuild, the derived gate
families, the consumer typechecks and the dogfood zoo file were in
progress when this PR opened. Their readings go into the `os-dev-report`
comment on #23488, and CI's check-runs on this PR are authoritative.
## Acceptance notes
- **Not edited:**
`packages/plugins/plugin-security/src/__tests__/legacy-catalog-objects.testkit.ts:333-336`.
It describes the retired `sys_permission_set.admin_scope` column as it
was, both keys included. That is accurate for a model of a retired
object; carrier: none.
- **Left as they are (PROSPECTIVE, not false):** three sentences
describe a door that ADR-0138 D3 item 4 assigns to the bind door when it
is built:
`packages/plugins/plugin-security/src/builtin-positions.ts:92`,
`packages/core/src/security/resolve-authz-context.ts:1099-1100`, and
`content/docs/permissions/permission-sets.mdx` "the wildcard ban is the
stricter `guest` tier's rule". Carrier: none.
- **Release-owned, not touched:** `content/docs/releases/v13.mdx:91` and
the `CHANGELOG.md` entries.
- **Out-of-repo stored rows and consumers:** NOT MEASURED, as the
reading stated.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec)!: retire the delegated-admin scope's `manageBindings` and `authorEnvironmentSets` — rights no door honoured (#23488)
9
+
10
+
**BREAKING** — `manageBindings` and `authorEnvironmentSets` on a permission set's `adminScope` (`AdminScopeSchema`, ADR-0090 D12) are now refused at parse. They named the right to bind sets to positions and the right to author environment permission sets, and they gated the `sys_position_permission_set` and `sys_permission_set` rows, which ADR-0131 D3 retired. A position's sets are part of its definition and a permission set is a definition; both are written through the metadata door, which admits a `manage_metadata` holder and reads no `adminScope`. So a delegate who "may bind" or "may author" was granted nothing. Delete both keys: a delegate's scope is placement, assignment and the allowlist.
11
+
12
+
Clause-②: yes (narrowing)
13
+
14
+
Measured before removal, at `cdc42eb49`: no production author set either key to `true` (the showcase wrote the default `false`; the control key `manageAssignments: true` has four production authors), and no door that writes a position or a permission set consulted either key. Their one reader was the delegated-admin gate's strict-containment comparison, which changes below.
| a delegate who re-composes what a position distributes, or authors a set's body | a `manage_metadata` holder writes the position or set definition through the metadata door (ADR-0131 D3); the delegate assigns people to positions and grants allowlisted sets inside its subtree |
23
+
| reading `scopes[].manageBindings` / `scopes[].authorEnvironmentSets` from `GET /api/v1/security/my-delegable-scope` (`DelegableAdminScope`) | nothing — the report carries `manageAssignments` as the scope's one action right |
24
+
25
+
**The one-line fix:** delete `manageBindings` and `authorEnvironmentSets` from every `adminScope`.
26
+
27
+
**What an author who still writes either sees.**`tsc` fails at the authoring site (`AdminScope` types both keys as retired), and the parse — `definePermissionSet()`, `defineStack({ permissions })`, `PUT /api/v1/meta/permission/:name` — refuses it at its own path with the prescription:
28
+
29
+
> `permission.adminScope.manageBindings` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once the position-to-permission-set binding rows it gated were retired: a position's sets are part of its definition (ADR-0131 D3), and that definition is written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. 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.
30
+
31
+
## The containment change (`@objectstack/plugin-security`)
32
+
33
+
The delegated-admin gate judges a delegate who hands out a set that itself carries an `adminScope` — a `sys_user_position` assignment of a position that distributes it, or a direct `sys_user_permission_set` grant — by **strict containment**. That comparison read both retired flags. It now reads `includeSubtree`, `manageAssignments` and the allowlist only:
34
+
35
+
- a delegate whose own scope set either flag `true` can **no longer** hand out a scope otherwise EQUAL to its own — the inert flag used to count as the strictly-larger axis and let that lateral copy through the equal-scope refusal. It is refused `403 PERMISSION_DENIED` (`… would hand out an adminScope not strictly contained by yours`);
36
+
- a granted scope that set either flag `true` no longer counts as out-reaching a delegate that did not.
37
+
38
+
`DelegatedAdminGate.describeDelegableScope()` (and so `GET /api/v1/security/my-delegable-scope`) no longer reports either flag.
39
+
40
+
## The retirement kit
41
+
42
+
-**Two `retiredKey()` tombstones** on `AdminScopeSchema`, a `strictObject` — so each refusal carries its prescription and `tsc` fails first. `RETIRED_KEYS_BY_MAJOR[18]`: `security/AdminScope:manageBindings`, `security/AdminScope:authorEnvironmentSets`. `DelegableAdminScope` (`@objectstack/spec/contracts`) drops both fields.
43
+
-**The D2 conversion `permission-admin-scope-binding-authoring-removed`** (protocol 18, retired from the load path) deletes both keys from every `permissions[].adminScope`, one notice per key, whatever they hold. Both keys carried the default `false`, so a 17.x parse materialized them in every declared scope; the rehydration seams replay this conversion over stored `permission` rows and built artifacts, and `os migrate meta --stored` lists the rows that carry them before the upgrade.
44
+
-**The D3 entry `permission-admin-scope-binding-authoring-retired`** records the containment change above: the judgement the strip cannot make for the upgrader.
45
+
-**`@objectstack/core`:**`permissionSetDefinitionFromRow` — the mapping `os migrate`'s legacy-row ceremony saves a `sys_permission_set` row through — drops both keys from the row's `admin_scope`. That save goes through `saveMetaItem`, which replays no conversion: measured, a 17.x row's body answered `422 INVALID_METADATA` at `adminScope.manageBindings`, and the same body without the keys saved.
46
+
-**`@objectstack/platform-objects`:** the permission-set form's `adminScope` help text (all four locales) names the scope's live keys only.
47
+
-**No deprecation window**, per the project's startup-stage posture.
48
+
49
+
⚠️ **The out-of-repo consumer population is NOT MEASURED.**`@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. Studio's two switches for these keys retire in an objectui follow-up.
Copy file name to clipboardExpand all lines: content/docs/permissions/delegated-administration.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -56,8 +56,8 @@ set, and the gate resolves the name in the security catalog. The gate's rules:
56
56
| Tenant admins pass through | The superuser wildcard reaches the ordinary CRUD/RLS checks — delegation constrains *delegates*, not HQ |
57
57
| Subtree anchoring | Assignments a delegate creates must be anchored (`sys_user_position.business_unit_id`) inside their subtree, and the target user must sit inside it |
58
58
| Allowlist, no self-escalation | Every set reached by the write — bound to the assigned position, or granted directly — must be on `assignablePermissionSets`. Granting an un-allowlisted set is refused **for anyone, including the delegate themselves**|
59
-
| Action flags|`manageAssignments` gates the assignment rows — `sys_user_position`, and direct `sys_user_permission_set` grants. `manageBindings` and `authorEnvironmentSets` gated writes to the binding rows and set rows of the retired `sys_position_permission_set` and `sys_permission_set` objects; neither object is registered any more, so neither flag admits a write. A position's sets are part of its definition and a permission set is a definition: both are written through the metadata door, which admits a `manage_metadata` holder (the platform operator under `group` or `isolated`) and reads no `adminScope` (ADR-0131 D3). The two flags still count in the strict-containment comparison below|
60
-
| Strict containment | Granting or authoring a set that itself carries an `adminScope` requires a held scope that **strictly contains** it — handing your own exact scope to a peer is refused (no lateral propagation) |
59
+
| Action flag|`manageAssignments` gates the assignment rows — `sys_user_position`, and direct `sys_user_permission_set` grants — and is the scope's only action right. A position's sets are part of its definition and a permission set is a definition: both are written through the metadata door, which admits a `manage_metadata` holder (the platform operator under `group` or `isolated`) and reads no `adminScope` (ADR-0131 D3). The scope's former `manageBindings` and `authorEnvironmentSets` named the retired `sys_position_permission_set` and `sys_permission_set` rows, admitted no write once those objects were gone, and are retired: the schema refuses both with the fix|
60
+
| Strict containment | Granting a set that itself carries an `adminScope`— assigning a position that distributes it, or granting it directly — requires a held scope that **strictly contains** it: the same root, no wider subtree, `manageAssignments` only if yours has it, an allowlist inside yours, and smaller on at least one of those. Handing your own exact scope to a peer is refused (no lateral propagation) |
61
61
| Single-row writes | Delegates write single rows by id only — a broad filter-write cannot be boundary-checked |
62
62
| Audit stamp | Every assignment a delegate creates is `granted_by`-stamped automatically |
63
63
| Tenant-level assets stay tenant-level | The `everyone`/`guest` audience anchors and security-domain publishes are untouchable from any delegated scope |
Copy file name to clipboardExpand all lines: content/docs/references/security/permission.mdx
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,8 +31,8 @@ const result = AdminScopeSchema.parse(data);
31
31
|**businessUnit**|`string`| ✅ |[ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root. Required and non-blank: an empty or whitespace-only value names no business unit and is refused. |
32
32
|**includeSubtree**|`boolean`| optional (default: `true`) | Cover descendant business units too (default true) |
33
33
|**manageAssignments**|`boolean`| optional (default: `false`) | Manage user↔position assignments within the subtree |
34
-
|**manageBindings**|`boolean`| optional (default: `false`) | Manage position↔permission-set bindings within the subtree|
35
-
|**authorEnvironmentSets**|`boolean`| optional (default: `false`) | Author environment-owned permission sets (single posture only: under group or isolated the platform operator authors the catalog, ADR-0131 D3)|
34
+
|**manageBindings**|`never`| optional |[REMOVED]`permission.adminScope.manageBindings` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once the position-to-permission-set binding rows it gated were retired: a position's sets are part of its definition (ADR-0131 D3), and that definition is written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. 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.|
35
+
|**authorEnvironmentSets**|`never`| optional |[REMOVED]`permission.adminScope.authorEnvironmentSets` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once permission sets stopped being rows: a permission set is an environment metadata definition (ADR-0131 D3), written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. 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.|
36
36
|**assignablePermissionSets**|`string[]`| optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out |
37
37
38
38
@@ -125,7 +125,7 @@ const result = AdminScopeSchema.parse(data);
125
125
|**systemPermissions**|`string[]`| optional | System level capabilities |
|**_lockReason**|`string`| optional | Human-readable reason shown when a write is refused by _lock. |
@@ -182,8 +182,8 @@ const result = AdminScopeSchema.parse(data);
182
182
|**businessUnit**|`string`| ✅ |[ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root. Required and non-blank: an empty or whitespace-only value names no business unit and is refused. |
183
183
|**includeSubtree**|`boolean`| optional (default: `true`) | Cover descendant business units too (default true) |
184
184
|**manageAssignments**|`boolean`| optional (default: `false`) | Manage user↔position assignments within the subtree |
185
-
|**manageBindings**|`boolean`| optional (default: `false`) | Manage position↔permission-set bindings within the subtree|
186
-
|**authorEnvironmentSets**|`boolean`| optional (default: `false`) | Author environment-owned permission sets (single posture only: under group or isolated the platform operator authors the catalog, ADR-0131 D3)|
185
+
|**manageBindings**|`never`| optional |[REMOVED]`permission.adminScope.manageBindings` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once the position-to-permission-set binding rows it gated were retired: a position's sets are part of its definition (ADR-0131 D3), and that definition is written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. 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.|
186
+
|**authorEnvironmentSets**|`never`| optional |[REMOVED]`permission.adminScope.authorEnvironmentSets` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once permission sets stopped being rows: a permission set is an environment metadata definition (ADR-0131 D3), written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. 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.|
187
187
|**assignablePermissionSets**|`string[]`| optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out |
0 commit comments