Repository navigation
Commit 7cda377
docs(adr): ADR-0090 D12 names the organization administrator's assignment boundary; ADR-0131 D3 notes it (#24163)
Fixes #24135
Clause-②: no
## Summary
Executes the maintainer's ruling of record `6228728551` on #23960
(letter B, 「同意」): the boundary for what an organization administrator
may assign is declared in the ADRs first, so the `plugin-security` card
(#24136, `Blocked-by:` this one; it remains open) can enforce it.
- **ADR-0090**
(`docs/adr/0090-permission-model-v2-concept-convergence.md`): one
status-line entry (the ADR's own convention for amendments, beside the
2026-10-15 entry for ruling `6181988435`) and one dated note under D12,
`Amended 2026-10-17` — the form D11's 2026-10-14 note uses. D12 now
names three kinds of administrator (tenant-level, organization-level,
delegated); the organization-level kind creates and removes
`sys_user_position` / `sys_user_permission_set` rows for its own
organization's members when every distributed set has no wildcard
superuser bit, no `adminScope` and only `scope: 'org'` capabilities;
`admin_full_access`, platform sets, `organization_admin` itself and
every delegated set stay outside; organization-administrator status is
conferred by membership grade only. Strict containment and the
`everyone` / `guest` anchors are unchanged. Ruling item 4's denylist
names the three D10 agent ceilings, which the three conditions
mis-settle; the general case behind them is held for the maintainer's
decision on #24164. The D12 text above the note is not rewritten.
- **ADR-0131**
(`docs/adr/0131-total-organization-ownership-no-null-organization-id.md`):
one dated note under D3's assignment paragraph, pointing at the D12
boundary as the rule that sentence runs under. It inherits the denylist
by reference.
- No code, no `packages/spec` key, no test, no migration text. The v18
entry's `replacement` text (`18.walled-catalog-tenant-writes-retired`)
stays as published.
## Patch round 1 (contract review `6232429140` on head `b0173e29c`,
FAIL)
The review found precision 5 of the previous head wrong for this
boundary: the three D10 agent ceilings pass the three conditions
mechanically but are not wall-bounded on their own. Re-measured here
before editing, on `origin/main` `eda6f14b7b`: `mcp_agent_data_read` is
`objects: { '*': { allowRead: true } }` with no `rowLevelSecurity`
(`default-permission-sets.ts:1427`–`:1435`); the seven SCIM projection
objects are `tenantField: null`, `reach: out` in
`scripts/platform-object-tenancy-census.json`; `fallbackPermissionSet:
null` composes the human baseline to `[]`
(`app-default-permission-set.ts:40`–`:57`). The head `61c8a5d34`
therefore:
1. moves `mcp_agent_data_read`, `mcp_agent_data_write` and
`mcp_agent_restricted` (the last by kind: an agent ceiling is not a
person's set) from "Inside" to "Outside (denylist)" in the D12 note's
measured paragraph, with the reason — an agent ceiling's bound is the
delegating principal's own grants (D10 rule 1), not the organization
wall; held by a person as a direct grant, the two reading ceilings carry
no row scope on the Layer-0-inert SCIM projection tables, and in a
walled deployment with no platform baseline they read every
organization's rows there, a read the granting organization
administrator does not hold;
2. replaces "names no set at this date" with "names those three sets",
keeping the clause that a set shipped later that the conditions cannot
settle joins the denylist by an amendment;
3. adds one sentence: the general case — an authored set with a `'*'`
read and no row scope on a Layer-0-inert readable object, in a
deployment with no platform baseline — is not settled by the note and is
held for the maintainer's decision on #24164. No fourth condition is
written;
4. tightens "refuses from every non-system writer" to "refuses from
every writer": `reserved_identity_position` takes no provenance
exemption (`sys-user-position.object.ts:225`–`:265`).
The rule paragraph of the note (the three conditions and the by-name
exclusions) is unchanged; the denylist lives in the measured paragraph,
as the review prescribed.
## Mechanism assumptions, measured on `origin/main` `eda6f14b7b`
- **A1 — holds.** ADR-0090 keeps amendments as dated `· **Amended** (…)`
status-line entries (`:4`–`:8`), and a superseding change also gets a
dated blockquote in the decision itself (D11's `> **Amended 2026-10-14
…**`; D3 / D5 / D8 carry `> **Note (2026-10-14) …**`). The 2026-10-15
D12 amendment is status-line only. This PR uses both: the status line,
and an `Amended 2026-10-17` blockquote at the end of D12 (D12 now at
`:515`).
- **A2 — holds.** ADR-0131 D3's assignment paragraph is `:322`–`:328`.
ADR-0131 itself already uses `> **Note (2026-10-14) — …**` blockquotes
(`:66`); the note follows that form. No status-line entry was added to
ADR-0131: the note changes no D3 text (its inline 2026-10-10 withdrawal
at `:315` set the precedent of a note without a status entry).
- **A3 — holds, with the exact fields.**
`packages/spec/src/security/capabilities.ts:33` declares
`PlatformCapability.scope: 'platform' | 'org'`, and every one of the 10
`PLATFORM_CAPABILITIES` rows sets it (`platform`: `manage_users`,
`manage_metadata`, `manage_platform_settings`, `setup.access`,
`studio.access`; `org`: `manage_org_users`, `setup.write`,
`manage_sharing`, `view_all_audit_log`, `manage_mailboxes`). A package
capability declares it through `CapabilityDeclarationSchema.scope`
(`:189`), `z.enum(['platform', 'org']).default('platform')`. The
superuser bits are `ObjectPermissionSchema.viewAllRecords` /
`modifyAllRecords` on the `objects['*']` entry; the delegation key is
`PermissionSetSchema.adminScope` (`permission.zod.ts:1022`). All three
conditions are readable from declarations alone.
- **A4 — measured; the conditions settle five of the eight shipped sets,
and ruling item 4's denylist names the other three (the D10 agent
ceilings).** Table below. The shipped roster is the eight sets in
`packages/plugins/plugin-security/src/objects/default-permission-sets.ts`,
held equal to `SHIPPED_PERMISSION_SET_NAMES`
(`packages/cli/src/utils/organization-ownership-inventory.ts:215`) by
that file's test; a repo-wide search for `PermissionSetSchema.parse(` /
`definePermissionSet(` outside tests finds no other shipped set
(fixtures in `packages/verify` and `packages/qa` only).
- **A5 — holds.** No heading was added (both notes are blockquotes), so
no anchor moved; `pnpm check:adr-anchors` exits 0 on the final head.
### Shipped permission sets against the three conditions
| set | (a) no `*` superuser bit | (b) no `adminScope` | (c) `org`
capabilities only | verdict |
|---|---|---|---|---|
| `admin_full_access` | no — `viewAllRecords` + `modifyAllRecords` | yes
| no — `manage_users`, `manage_metadata`, `manage_platform_settings`,
`setup.access`, `studio.access` | **outside** |
| `organization_admin` | no — `viewAllRecords` + `modifyAllRecords` |
yes | no — `setup.access` | **outside** (also by name) |
| `organization_admin_no_bypass` | yes — the derivation drops both bits
| yes | no — `setup.access` | **outside** (also by name) |
| `member_default` | yes — no `*` entry at all | yes | yes — none |
inside |
| `viewer_readonly` | yes — `allowRead` only | yes | yes — none | inside
(it carries the SCIM row scope itself) |
| `mcp_agent_data_read` | yes — `allowRead` only, no row scope | yes |
yes — none | **outside (denylist)** — mis-settled |
| `mcp_agent_data_write` | yes — CRUD bits only, no row scope | yes |
yes — none | **outside (denylist)** — mis-settled |
| `mcp_agent_restricted` | yes — `objects: {}` | yes | yes — none |
**outside (denylist)** — by kind |
Shipped positions: the four ADR-0068 D2 identity names
(`platform_admin`, `org_owner`, `org_admin`, `org_member`) are refused
on `sys_user_position.position` for every writer by the object
validation `reserved_identity_position`
(`sys-user-position.object.ts:256`, no provenance exemption), and the
two anchors by the gate's unconditional anchor refusal
(`delegated-admin-gate.ts:298`–`:312`). Neither is a question for this
boundary; the note records them as unchanged.
## Precisions the contract review should read
Each narrows; none widens the ruled rule.
1. **"Tenant-level" is the platform administrator alone.** ADR-0095
names the organization administrator's posture rung `TENANT_ADMIN`; the
note says in so many words that D12's "tenant-level" is the first kind
only, so the two vocabularies cannot be read into each other.
2. **Condition (c) is fail-closed on an undeclared scope.** A package
declaration that omits `scope` defaults to `'platform'`, and a
`systemPermissions` name no declaration carries is not an `org`
capability.
3. **"Each set a position carries" includes the position-name fold.**
The resolver requests `[...positions, ...explicitPermissionSets]`
(`security-plugin.ts:6542`), so a held position also grants the
permission set of its own name; the note counts that set while the fold
stands. Without this a position could carry an outside set past the
boundary.
4. **Both organization-administrator variants are outside by name.**
`organization_admin_no_bypass` is outside by (c) today only because
`setup.access` is `platform`; the note also excludes it by name, which
is what "conferred by membership grade only" requires.
5. **The three D10 agent ceilings are outside, on ruling item 4's
denylist.** The conditions place them inside and so mis-settle them: an
agent ceiling's bound is the delegating principal's own grants (D10 rule
1), not the organization wall. Held by a person as a direct grant there
is no intersection, and the two reading ceilings carry no row scope on
the seven Layer-0-inert SCIM projection tables; in a walled deployment
with no platform baseline (`fallbackPermissionSet: null`) they read
every organization's rows there, a read the granting organization
administrator does not hold (its explicit per-object entries keep the
`_self` / `_none` scope). `mcp_agent_restricted` is named by kind. The
general case — any authored set with a `'*'` read and no row scope on a
Layer-0-inert readable object, in a no-baseline deployment — is not
settled by the conditions or by a shipped-set denylist; the note holds
it for the maintainer's decision on #24164 and writes no fourth
condition.
6. **Create and remove only** — the ruling's `allowCreate` /
`allowDelete` on, `allowEdit` off.
## Gates (final head `61c8a5d34`)
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 19 commands from the tree at
`61c8a5d34` (change set: the two ADR files, +76 / -0 against the base;
the same 19 on every head of this PR). Each ran on `61c8a5d34` with the
exit code captured before any pipe:
- `node scripts/check-adr-links.mjs` :: exit 0 — "769 relative link
destination(s) under docs/adr/ resolve"; `--self-test` :: exit 0
- `node scripts/check-adr-symbol-anchors.mjs` :: exit 0 — "2353 anchors
across 142 records resolve"; `--self-test` :: exit 0
- `node scripts/check-ci-filter-parity.mjs` :: exit 0
- `node scripts/check-closing-keyword-parity.mjs` :: exit 0;
`--self-test` :: exit 0
- `node scripts/check-comment-mask-corpus.mjs` :: exit 0 — "9404 files,
0 disagree"
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions` ::
exit 0 (its prerequisites `@objectstack/formula` and `@objectstack/lint`
built first under the verify lock, `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint
--concurrency=2`, VERDICT command-exit 0; on the first head the gate had
answered exit 3, PREREQUISITE NOT MET, before that build)
- `pnpm check:adr-anchors` :: exit 0 — "check-adr-anchors: OK (70
anchored file(s), every governing ADR still referenced; 136 decision
number(s) …; 47869 citation(s) across 6347 file(s) resolve …)"
- `pnpm check:cross-package-test-inputs`, `check:doc-authoring`,
`check:driver-memory-census`, `check:gitlink-declared`,
`check:nul-bytes`, `check:pm-governed-merges`, `check:pm-prior-rulings`,
`check:refd-timer-probe`, `check:watch-hint-literal` :: exit 0 each
`--ran` reconciliation: "19 derived famil(ies) accounted for — 19 run, 0
NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of
them is 3)".
`node scripts/pm/check-governed-merges.mjs --branch
claude/issue-24135-adr-0090-d12-org-admin-boundary` (exit 3, the
governed code): "GOVERNED — a human merge is the review record for this
PR", "landing tier: H(人合)", `docs/adr/**` ×2, under the 3000-line
threshold.
Everything else CI runs on this PR is CI's; nothing was re-run locally.
No package builds or tests are owed: the diff touches no package.
## Acceptance notes
Read-only inferences from the measurement above, not reproduced and not
filed:
- The delegated gate's position walk (`setsBoundToPosition`,
`delegated-admin-gate.ts:1086`) reads only the definition's
`permissionSets`, while the resolver also grants the set of the
position's own name (precision 3). A delegate's allowlist check
therefore does not see a same-named set. Carrier: #24136, whose third
branch reuses that walk.
- `isTenantAdmin` (`delegated-admin-gate.ts:256`, a wildcard
`modifyAllRecords` holder) reads the walled `organization_admin` as
tenant-level and the wall-less variant as no administrator at all; under
the amended D12 both are organization-level. Carrier: #24136.
- The companion reference `docs/design/permission-model.md` §10
describes delegated administration only; the organization-level kind
belongs there once #24136 enforces it (docs follow the implementation).
Carrier: #24136.
- The D10 agent ceilings as a person's direct grant: the shipped
instance is now on the item-4 denylist in this PR (precision 5); the
general class — an authored wildcard-read set with no row scope on a
Layer-0-inert object, in a no-baseline deployment — is the maintainer's
decision on #24164, filed by the seat.
## 维护者速读(草稿)
### 改了什么
在权限模型 ADR-0090 的 D12(管理权限)里加了一条带日期的修订,并在 ADR-0131 D3 下加了一行注记,纯文字,不改代码、不加
spec 键。修订把管理者分成三类:租户级(平台管理员)、组织级(组织的 owner / admin,自动获得组织管理员权限集)、委托级(带
`adminScope`
的人)。组织级管理员可以给**本组织成员**增加或移除岗位与权限集,但只限"被组织墙约束住"的集:不带通配超级用户位、不带
`adminScope`、所含能力全部是组织级。`admin_full_access`、平台级集、组织管理员集本身、任何委托集都不能分;组织管理员身份只能由成员资格(owner
/ admin)获得,不能靠分配复制。本轮按席位复核意见补了一条:三个给 AI 代理用的 MCP
上限权限集列入裁决预留的黑名单,组织管理员不能把它们分给人。
### 为什么改
这是您 10 月 17 日对 #23960 的裁决 B(「同意」)的第一步。ADR-0131 D3
承诺"组织的管理员给本组织成员分配",但代码为了防止组织管理员给自己绑 `admin_full_access`
越过组织墙,把两张分配表设成了只读;缺的正是"组织管理员能分配哪些"这条边界。边界先写进 ADR,代码卡
#24136(被本卡阻塞)才能开工。实测出货的 8 个权限集:三条件能判定 5 个(2 个在内、3 个在外);剩下 3 个 MCP
代理上限集按三条件看似在内,但它们本来只配给 AI 代理、靠委托人自己的权限收窄,直接分给人时在"不配平台基线"的部署里能读到其他组织的
SCIM 身份数据,而分配它的组织管理员自己都读不到——所以按裁决第 4 条列入黑名单。
### 风险与代价(含回滚)
只改两份 ADR 的文字,今天的产品行为不变:两张分配表仍然只读,直到 #24136
落地。规则措辞上的几处精确化全部是收紧、没有放宽:未声明范围的能力按平台级算;岗位按名字带出的同名权限集也算"岗位携带的集";无墙部署用的
`organization_admin_no_bypass` 也按名字排除;三个 MCP 上限集列入黑名单。还有一个更一般的口子没有在本 ADR
里定:任何自建的"通配只读、没有行级范围"的权限集,在不配平台基线的部署里同样能碰到那几张没有组织列的 SCIM
表——是否加第四个条件,还是把保证写成"以平台基线为前提",已单独立成待您决策的 #24164,本 PR 不替您决定。回滚:直接 revert
本 PR,不涉及数据和发布。
### 席位意见
### 你要做的(一个动作)
席位复核通过后,在本 PR 上点 Approve。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent eda6f14 commit 7cda377
2 files changed
Lines changed: 76 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
531 | 532 | | |
532 | 533 | | |
533 | 534 | | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
534 | 604 | | |
535 | 605 | | |
536 | 606 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
327 | 327 | | |
328 | 328 | | |
329 | 329 | | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
330 | 336 | | |
331 | 337 | | |
332 | 338 | | |
| |||
0 commit comments