From cf0b3cd9ddf50c8cb51e427ca849f734acf9cf90 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 11:54:33 +0000 Subject: [PATCH 1/4] spec(security): refuse a blank AdminScope businessUnit anchor at parse AdminScopeSchema.businessUnit is the scope's one required key, and a bare string accepted '' and whitespace-only values, satisfying the requirement while naming no business unit. It now carries the non-transforming NON_BLANK_STRING refinement with a message naming what a valid anchor is (the sys_business_unit.name of the subtree root). A real name parses byte-identical; the absent key keeps its own invalid_type refusal. Stored scopes are not rewritten: the ADR-0087 semantic entry admin-scope-business-unit-blank-refused records that a stored blank anchor is refused on its next write. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- ...admin-scope-business-unit-blank-refused.ts | 68 +++++++++++++++++++ packages/spec/src/security/permission.test.ts | 57 ++++++++++++++++ packages/spec/src/security/permission.zod.ts | 45 +++++++++++- 3 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts diff --git a/packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts b/packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts new file mode 100644 index 00000000000..eaef7f83927 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.admin-scope-business-unit-blank-refused.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// The delegated-admin scope's one required key, required NON-BLANK. Stored +// scopes are deliberately not rewritten: the root of a subtree cannot be +// inferred from a blank, so the stored row is refused on its next write and +// named by the boot reconciliation's existing durability error instead. +export const entry: SemanticMigration = { + id: 'admin-scope-business-unit-blank-refused', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a + // code span already, and a nested backtick would close it. + surface: + 'security.permission.adminScope.businessUnit (AdminScopeSchema, ADR-0090 D12) authored or stored ' + + 'BLANK: the empty string, or a value that is nothing but whitespace (spaces, a tab, a newline). ' + + 'The key is the scope\'s only required one and names the root business unit of the delegated ' + + 'subtree; a blank value satisfied the requirement while naming no unit', + replacement: + 'the sys_business_unit.name (machine name) of the business unit at the root of the subtree the ' + + 'delegate administers, written out: `businessUnit: \'north_america\'`. If the permission set ' + + 'should not delegate administration at all, remove `adminScope` from it. ⛔ There is no ' + + 'replacement that can be DERIVED from what was written: a blank names no unit, so the root the ' + + 'author meant is not recoverable, and the platform must not pick one.', + reason: + 'Maintainer ruling A on #19461 (decision batch #217 item 1, 2026-09-23 「217 同意」). ' + + '`AdminScopeSchema` declared `businessUnit` as a bare string with no minimum, so ' + + '`{ businessUnit: \'\' }` and `{ businessUnit: \' \' }` parsed green — measured against the ' + + 'published spec 17.4.0 and re-measured on `main` before the change. This narrows a published ' + + 'face: every other key of the scope is scoped TO this one, and ADR-0090 D12 declares the scope\'s ' + + 'WHERE as a business-unit subtree, which a blank does not name. The delegated-admin gate ' + + 'resolves the anchor by exact name, so a blank anchor resolves to an empty subtree and approves ' + + 'nothing on the subtree axes — no escalation was measured; the defect is a declaration that ' + + 'does not enforce what it declares, satisfied most readily by an author (an AI author above ' + + 'all) that knew the key was required and did not yet know the unit. The refusal is a ' + + 'NON-TRANSFORMING refinement at the key\'s own path, deliberately not a trim: the metadata save ' + + 'path persists the submitted body verbatim rather than the parsed value, so a trimming schema ' + + 'would validate one string and store another that the gate\'s exact lookup cannot resolve. A ' + + 'real name therefore parses byte-identical. Scope is blankness only: a real name with ' + + 'surrounding whitespace is not judged by this entry. ⚠️ STORED ROWS ARE NOT REWRITTEN and there ' + + 'is no D2 conversion (no lossless rewrite exists — the root cannot be inferred, and dropping ' + + 'the scope would silently change who is a delegate). The read path does not re-validate stored ' + + 'rows, so no stored permission set becomes unreadable. A stored blank-anchored scope is refused ' + + 'on its NEXT WRITE instead: a Setup or data-door edit of that permission set answers 422 ' + + 'INVALID_METADATA naming adminScope.businessUnit, and the boot reconciliation backfill of a ' + + 'legacy record with no metadata definition reports it through its existing durability ERROR ' + + '(ADR-0094 D4), whose own prescription is to make the record body spec-valid; restoring a ' + + 'trashed blank-anchored set brings the record back and reports the missing definition at ERROR ' + + 'the same way. ⛔ No path skips the row. Ships at once, no grace window and no dual spelling ' + + '(2026-08-27 maintainer ruling 「短期不考虑渐进」). ADR-0049 / ADR-0087 / ADR-0090.', + acceptanceCriteria: + 'Search every authored and stored permission set for an `adminScope` whose `businessUnit` is ' + + 'empty or whitespace-only — metadata files, `sys_metadata` permission rows, and the ' + + '`admin_scope` column of `sys_permission_set` — and write the root unit\'s machine name, or ' + + 'remove `adminScope` where the set should not delegate. The sweep is mechanical for authored ' + + 'metadata: `AdminScopeSchema.safeParse` and `PermissionSetSchema.safeParse` answer exactly one ' + + '`custom` issue at `businessUnit` (or `adminScope.businessUnit` when the set is parsed whole) ' + + 'naming `sys_business_unit.name` as the valid anchor. For STORED rows the search above is the ' + + 'catch-all, because reads never refuse: a definition already stored in `sys_metadata` with a ' + + 'blank anchor loads and resolves exactly as before and says nothing until it is written again. ' + + 'Two write paths then name it: a re-save of the set (refused 422 at adminScope.businessUnit), ' + + 'and — for a legacy `sys_permission_set` record with no metadata definition only — the boot ' + + 'log, where the ADR-0094 D4 backfill\'s first-failure ERROR names the record and carries the ' + + 'offending key and its summary ERROR counts and names the failed records. ⛔ A clean boot is ' + + 'therefore NOT a completed sweep. An absent `businessUnit` is refused exactly as before, with its own invalid_type issue. Nothing is ' + + 'normalised on the way through: an accepted anchor arrives byte-identical to what was written. ' + + 'The repo and example apps carried zero blank anchors at the time of the change, so no fixture ' + + 'had to be rewritten.', +}; diff --git a/packages/spec/src/security/permission.test.ts b/packages/spec/src/security/permission.test.ts index d1f1583838f..631b0d55e94 100644 --- a/packages/spec/src/security/permission.test.ts +++ b/packages/spec/src/security/permission.test.ts @@ -46,6 +46,63 @@ describe('AdminScopeSchema (ADR-0090 D12)', () => { it('requires the businessUnit boundary', () => { expect(() => AdminScopeSchema.parse({ manageAssignments: true })).toThrow(); }); + + // [#19461] The one required key is required NON-BLANK: an empty or + // whitespace-only anchor names no business unit, so it satisfied the + // requirement vacuously. Each refusal is exactly ONE issue at the key's own + // path, and its message names what a valid anchor is. + describe('refuses a blank businessUnit anchor', () => { + const blanks: ReadonlyArray = [ + ['the empty string', ''], + ['spaces only', ' '], + ['a tab only', '\t'], + ]; + + it.each(blanks)('refuses %s at businessUnit', (_label, businessUnit) => { + const result = AdminScopeSchema.safeParse({ businessUnit, manageAssignments: true }); + expect(result.success).toBe(false); + const issues = result.error!.issues; + expect(issues).toHaveLength(1); + expect(issues[0]!.code).toBe('custom'); + expect(issues[0]!.path).toEqual(['businessUnit']); + expect(issues[0]!.message).toContain('sys_business_unit.name'); + }); + + it.each(blanks)('refuses %s through PermissionSetSchema.adminScope', (_label, businessUnit) => { + const result = PermissionSetSchema.safeParse({ + name: 'east_subsidiary_admin', + objects: {}, + adminScope: { businessUnit, manageAssignments: true }, + }); + expect(result.success).toBe(false); + const issues = result.error!.issues; + expect(issues).toHaveLength(1); + expect(issues[0]!.code).toBe('custom'); + expect(issues[0]!.path).toEqual(['adminScope', 'businessUnit']); + expect(issues[0]!.message).toContain('sys_business_unit.name'); + }); + + it('accepts a real business-unit name, unchanged by the parse', () => { + expect(AdminScopeSchema.parse({ businessUnit: 'north_america' }).businessUnit).toBe('north_america'); + // The refinement is NON-TRANSFORMING: the parse never rewrites the + // anchor, because the stored body is the submitted one and the gate + // looks the anchor up by exact name. Padding is kept byte-identical — + // this pins the absence of a transform, not a verdict that padding is + // wanted. + expect(AdminScopeSchema.parse({ businessUnit: ' north_america ' }).businessUnit).toBe(' north_america '); + }); + + it('keeps the absent-key refusal exactly as before: one invalid_type at businessUnit', () => { + const result = AdminScopeSchema.safeParse({ manageAssignments: true }); + expect(result.success).toBe(false); + const issues = result.error!.issues; + expect(issues).toHaveLength(1); + expect(issues[0]!.code).toBe('invalid_type'); + expect(issues[0]!.path).toEqual(['businessUnit']); + expect((issues[0] as { expected?: string }).expected).toBe('string'); + expect(issues[0]!.message).not.toContain('sys_business_unit.name'); + }); + }); }); describe('ObjectPermissionSchema', () => { diff --git a/packages/spec/src/security/permission.zod.ts b/packages/spec/src/security/permission.zod.ts index 0e6d6902b06..0dddd0bdb43 100644 --- a/packages/spec/src/security/permission.zod.ts +++ b/packages/spec/src/security/permission.zod.ts @@ -7,6 +7,7 @@ import { ApiOperationSchema } from '../data/object.zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; import { lazySchema } from '../shared/lazy-schema'; +import { NON_BLANK_STRING } from '../shared/refinement-projection'; import { acceptRetiredDefaultResidue, retiredKey } from '../shared/retired-key'; import { strictObject } from '../shared/strict-object'; @@ -616,6 +617,23 @@ export const EffectiveObjectPermissionSchema = lazySchema(() => ); export type EffectiveObjectPermission = z.input; +/** + * [#19461] The one sentence a blank {@link AdminScopeSchema} `businessUnit` + * is refused with. It names what a valid anchor IS, because the author most + * likely to write a blank one is an agent that knew the key was required and + * did not yet know the unit. + * + * Module-private, like {@link checkScopeAgainstSuperUserBits}: its one + * consumer is the refinement below, and exporting it would add a constant to + * the published API surface for no caller. + */ +const ADMIN_SCOPE_BUSINESS_UNIT_BLANK = + 'A blank businessUnit is not a delegation boundary: businessUnit is the sys_business_unit.name ' + + '(machine name) of the business unit at the root of the subtree this scope delegates, and an ' + + 'empty or whitespace-only value names no unit, so the scope anchors no subtree. Write the root ' + + 'unit\'s machine name, e.g. businessUnit: \'north_america\', or remove adminScope if this ' + + 'permission set should not delegate administration.'; + /** * [ADR-0090 D12] Delegated-administration scope. * @@ -641,8 +659,31 @@ export const AdminScopeSchema = lazySchema(() => strictObject( 'boundary the author intended was never enforced.', }, { - /** Root of the delegated subtree — `sys_business_unit.name` (machine name, portable across environments). */ - businessUnit: z.string().describe('[ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root'), + /** + * Root of the delegated subtree — `sys_business_unit.name` (machine name, + * portable across environments). Required, and required NON-BLANK. + * + * [#19461] The key is the scope's only required one, and every other key is + * scoped TO it, so an empty or whitespace-only value satisfied the + * requirement while naming no subtree. Refused here with a NON-TRANSFORMING + * refinement, deliberately never `.trim()`: `saveMetaItem` persists the + * submitted body verbatim rather than the parsed value, so a transform would + * validate one string and store another — and the delegated-admin gate looks + * the anchor up by exact name. A real name therefore parses byte-identical, + * padding included. The absent key keeps its own `invalid_type` refusal: the + * refinement never runs on a non-string. + * + * Stored scopes are not rewritten (no lossless rewrite exists — the root + * cannot be inferred): a stored blank anchor is refused on its next write, + * and the ADR-0087 semantic entry `admin-scope-business-unit-blank-refused` + * records it. + */ + businessUnit: z.string() + .refine(NON_BLANK_STRING, { message: ADMIN_SCOPE_BUSINESS_UNIT_BLANK }) + .describe( + '[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.', + ), /** Whether the scope covers the whole subtree under `businessUnit` (default) or that single unit only. */ includeSubtree: z.boolean().default(true).describe('Cover descendant business units too (default true)'), /** May create/update/delete `sys_user_position` assignments (and direct `sys_user_permission_set` grants) within the boundary. */ From 6c85021ce43e11f946a44ccb4d589927d4336efb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 11:54:55 +0000 Subject: [PATCH 2/4] chore(spec): regenerate the migration registry for the admin-scope blank-anchor entry Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/spec/src/migrations/registry.ts | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 0d0cc134a12..89247672f0d 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5489,6 +5489,70 @@ const step18: MigrationStep = { + 'exportable by nobody, which is the point of the change; confirm that is what you want ' + 'before granting it back.', }, + // The delegated-admin scope's one required key, required NON-BLANK. Stored + // scopes are deliberately not rewritten: the root of a subtree cannot be + // inferred from a blank, so the stored row is refused on its next write and + // named by the boot reconciliation's existing durability error instead. + { + id: 'admin-scope-business-unit-blank-refused', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a + // code span already, and a nested backtick would close it. + surface: + 'security.permission.adminScope.businessUnit (AdminScopeSchema, ADR-0090 D12) authored or stored ' + + 'BLANK: the empty string, or a value that is nothing but whitespace (spaces, a tab, a newline). ' + + 'The key is the scope\'s only required one and names the root business unit of the delegated ' + + 'subtree; a blank value satisfied the requirement while naming no unit', + replacement: + 'the sys_business_unit.name (machine name) of the business unit at the root of the subtree the ' + + 'delegate administers, written out: `businessUnit: \'north_america\'`. If the permission set ' + + 'should not delegate administration at all, remove `adminScope` from it. ⛔ There is no ' + + 'replacement that can be DERIVED from what was written: a blank names no unit, so the root the ' + + 'author meant is not recoverable, and the platform must not pick one.', + reason: + 'Maintainer ruling A on #19461 (decision batch #217 item 1, 2026-09-23 「217 同意」). ' + + '`AdminScopeSchema` declared `businessUnit` as a bare string with no minimum, so ' + + '`{ businessUnit: \'\' }` and `{ businessUnit: \' \' }` parsed green — measured against the ' + + 'published spec 17.4.0 and re-measured on `main` before the change. This narrows a published ' + + 'face: every other key of the scope is scoped TO this one, and ADR-0090 D12 declares the scope\'s ' + + 'WHERE as a business-unit subtree, which a blank does not name. The delegated-admin gate ' + + 'resolves the anchor by exact name, so a blank anchor resolves to an empty subtree and approves ' + + 'nothing on the subtree axes — no escalation was measured; the defect is a declaration that ' + + 'does not enforce what it declares, satisfied most readily by an author (an AI author above ' + + 'all) that knew the key was required and did not yet know the unit. The refusal is a ' + + 'NON-TRANSFORMING refinement at the key\'s own path, deliberately not a trim: the metadata save ' + + 'path persists the submitted body verbatim rather than the parsed value, so a trimming schema ' + + 'would validate one string and store another that the gate\'s exact lookup cannot resolve. A ' + + 'real name therefore parses byte-identical. Scope is blankness only: a real name with ' + + 'surrounding whitespace is not judged by this entry. ⚠️ STORED ROWS ARE NOT REWRITTEN and there ' + + 'is no D2 conversion (no lossless rewrite exists — the root cannot be inferred, and dropping ' + + 'the scope would silently change who is a delegate). The read path does not re-validate stored ' + + 'rows, so no stored permission set becomes unreadable. A stored blank-anchored scope is refused ' + + 'on its NEXT WRITE instead: a Setup or data-door edit of that permission set answers 422 ' + + 'INVALID_METADATA naming adminScope.businessUnit, and the boot reconciliation backfill of a ' + + 'legacy record with no metadata definition reports it through its existing durability ERROR ' + + '(ADR-0094 D4), whose own prescription is to make the record body spec-valid; restoring a ' + + 'trashed blank-anchored set brings the record back and reports the missing definition at ERROR ' + + 'the same way. ⛔ No path skips the row. Ships at once, no grace window and no dual spelling ' + + '(2026-08-27 maintainer ruling 「短期不考虑渐进」). ADR-0049 / ADR-0087 / ADR-0090.', + acceptanceCriteria: + 'Search every authored and stored permission set for an `adminScope` whose `businessUnit` is ' + + 'empty or whitespace-only — metadata files, `sys_metadata` permission rows, and the ' + + '`admin_scope` column of `sys_permission_set` — and write the root unit\'s machine name, or ' + + 'remove `adminScope` where the set should not delegate. The sweep is mechanical for authored ' + + 'metadata: `AdminScopeSchema.safeParse` and `PermissionSetSchema.safeParse` answer exactly one ' + + '`custom` issue at `businessUnit` (or `adminScope.businessUnit` when the set is parsed whole) ' + + 'naming `sys_business_unit.name` as the valid anchor. For STORED rows the search above is the ' + + 'catch-all, because reads never refuse: a definition already stored in `sys_metadata` with a ' + + 'blank anchor loads and resolves exactly as before and says nothing until it is written again. ' + + 'Two write paths then name it: a re-save of the set (refused 422 at adminScope.businessUnit), ' + + 'and — for a legacy `sys_permission_set` record with no metadata definition only — the boot ' + + 'log, where the ADR-0094 D4 backfill\'s first-failure ERROR names the record and carries the ' + + 'offending key and its summary ERROR counts and names the failed records. ⛔ A clean boot is ' + + 'therefore NOT a completed sweep. An absent `businessUnit` is refused exactly as before, with its own invalid_type issue. Nothing is ' + + 'normalised on the way through: an accepted anchor arrives byte-identical to what was written. ' + + 'The repo and example apps carried zero blank anchors at the time of the change, so no fixture ' + + 'had to be rewritten.', + }, { id: 'advanced-plugin-lifecycle-config-retired', surface: From bac98769ea230895fab7f11413afd1d644e6038b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 12:07:13 +0000 Subject: [PATCH 3/4] chore(changeset): admin-scope blank businessUnit anchor refused Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- ...admin-scope-business-unit-blank-refused.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .changeset/19461-admin-scope-business-unit-blank-refused.md diff --git a/.changeset/19461-admin-scope-business-unit-blank-refused.md b/.changeset/19461-admin-scope-business-unit-blank-refused.md new file mode 100644 index 00000000000..9d88aace95e --- /dev/null +++ b/.changeset/19461-admin-scope-business-unit-blank-refused.md @@ -0,0 +1,53 @@ +--- +'@objectstack/spec': minor +--- + +**BREAKING for authored metadata** — a delegated-admin scope's `businessUnit` (`AdminScopeSchema`, reached as `adminScope.businessUnit` on a permission set) must now name a business unit. An empty or whitespace-only value is refused at parse, at the key's own path, with a message naming what a valid anchor is: the `sys_business_unit.name` of the root of the delegated subtree (#19461). + +Clause-②: yes + +Maintainer ruling A on decision batch #217 item 1, 2026-09-23 「217 同意」. + +## What changed, and why + +`businessUnit` is the scope's one required key, and every other key of the scope is scoped to it. It was declared as a bare string with no minimum, so `{ businessUnit: '' }` and `{ businessUnit: ' ' }` parsed green: the requirement was satisfied by a value that names no business unit. The delegated-admin gate looks the anchor up by exact name, so a blank anchor resolved to an empty subtree. No escalation was measured; the defect is a declaration that did not enforce what it declared. The likeliest author of a blank anchor is an AI that knew the key was required and did not yet know the unit, and until now the platform answered "accepted". + +``` +FROM AdminScopeSchema.safeParse({ businessUnit: '' }) + -> { success: true } + +TO AdminScopeSchema.safeParse({ businessUnit: '' }) + -> { success: false, + issues: [{ code: 'custom', path: ['businessUnit'], + message: 'A blank businessUnit is not a delegation boundary: businessUnit is + the sys_business_unit.name (machine name) of the business unit at + the root of the subtree this scope delegates, …' }] } +``` + +The refusal is a non-transforming refinement: nothing is trimmed. The metadata save path stores the submitted body as written, so a trimming schema would validate one string and store another. A real name parses byte-identical. The published JSON Schema states the same rule (`minLength: 1` and a non-whitespace `pattern`). + +## Migration — FROM → TO + +| You wrote | Write instead | +| --- | --- | +| `adminScope: { businessUnit: '' }` | `adminScope: { businessUnit: 'north_america' }`, using the machine name of the unit at the root of the subtree | +| `adminScope: { businessUnit: ' ' }` (or a tab or newline) | the same: the root unit's `sys_business_unit.name` | +| a blank-anchored scope on a set that should not delegate at all | remove `adminScope` from the permission set | + +**The one-line fix: set `businessUnit` to the `sys_business_unit.name` of the business unit at the root of the delegated subtree, or remove `adminScope` if the set should not delegate administration.** This cannot be converted automatically, because a blank names no unit and the intended root cannot be inferred. So this ships as an ADR-0087 D3 structured TODO with **no D2 conversion**. + + + +## Stored permission sets + +- **Stored scopes are not rewritten.** Reads do not re-validate stored rows, so no stored permission set becomes unreadable. A stored blank anchor loads and resolves exactly as before. +- **The next write refuses it.** A Setup or data-door edit of a permission set whose stored scope has a blank anchor answers `422 INVALID_METADATA` naming `adminScope.businessUnit`, until the anchor is named or the scope removed. +- **The boot reconciliation backfill reports it.** A legacy `sys_permission_set` record with no metadata definition and a blank anchor is not backfilled. It is reported on every boot through the existing ADR-0094 D4 durability `ERROR`, which names the record and the offending key, until the record is fixed or deleted. Restoring a trashed blank-anchored set brings the record back and reports the missing definition at `ERROR` the same way. No path skips the row. +- **A clean boot is not a completed sweep.** A definition already stored in `sys_metadata` says nothing until it is written again, so search stored permission sets and the `admin_scope` column for a blank `businessUnit`. + +## What does NOT change + +- **An absent `businessUnit`** is refused exactly as before, with its own `invalid_type` issue. +- **A real name with surrounding whitespace** is not judged by this change. It parses and is stored byte-identical. +- **The other keys of the scope** (`includeSubtree`, `manageAssignments`, `manageBindings`, `authorEnvironmentSets`, `assignablePermissionSets`) are untouched. +- **The published export surface.** No export is added, removed or renamed, and the `AdminScope` / `AdminScopeParsed` types are unchanged. From a5a53acaf09d2203735cc86d9ce57e5301c20b54 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 12:07:47 +0000 Subject: [PATCH 4/4] docs(references): regenerate the permission reference for the non-blank businessUnit rule Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- content/docs/references/security/permission.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/references/security/permission.mdx b/content/docs/references/security/permission.mdx index d98e3febd5d..376ef2c3f83 100644 --- a/content/docs/references/security/permission.mdx +++ b/content/docs/references/security/permission.mdx @@ -27,7 +27,7 @@ const result = AdminScopeSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **businessUnit** | `string` | ✅ | [ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root | +| **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. | | **includeSubtree** | `boolean` | optional (default: `true`) | Cover descendant business units too (default true) | | **manageAssignments** | `boolean` | optional (default: `false`) | Manage user↔position assignments within the subtree | | **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree | @@ -178,7 +178,7 @@ const result = AdminScopeSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **businessUnit** | `string` | ✅ | [ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root | +| **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. | | **includeSubtree** | `boolean` | optional (default: `true`) | Cover descendant business units too (default true) | | **manageAssignments** | `boolean` | optional (default: `false`) | Manage user↔position assignments within the subtree | | **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree |