Skip to content

docs(adr): ADR-0090 D12 names the organisation administrator's assignment boundary (wall-bounded sets only) and ADR-0131 D3 notes it (ruling B on #23960) #24135

Closed

Description

@objectstack-fleet

Filed by the director seat (summon #43 continuation 3, session_01QJC7i981TSs9L1CGJK59Ax) on the maintainer's ruling B on #23960 (batch #358 item 2, 「同意」 on the seat's revised recommendation; the ruling record is that card's Ruled: line). ⛔ Not a claim. Filing gate ①: a published decision the runtime contradicts, with the missing rule named. Tier H (docs/adr/**): lands only after an authorised approval with an at-tier ## Contract review record.

What is measured (objectstack origin/main be076dad28a0)

  • ADR-0131 D3 (docs/adr/0131-total-organization-ownership-no-null-organization-id.md:323–:326): assignments 「are created and removed by that organization's admins for its own members」. ADR-0090 D12 (docs/adr/0090-permission-model-v2-concept-convergence.md:514): administration is a scoped capability with an allowlist per delegated scope; nothing names what the built-in organization_admin may assign.
  • The shipped set keeps sys_user_position and sys_user_permission_set read-only 「to prevent privilege escalation」 (packages/plugins/plugin-security/src/objects/default-permission-sets.ts:441–, pinned by rbac-objects.test.ts:63), because with no boundary an org admin could bind admin_full_access to themselves and leave the wall. Measured on security: the 18 migration text says organization admins assign positions and permission sets to their members; the shipped organization_admin sets are read-only on both assignment objects and the server refuses #23960: every assignment write by an org admin answers 403; the v18 upgrade text (18.walled-catalog-tenant-writes-retired.ts, replacement) repeats D3.

Scope — the rule to write

  1. ADR-0090 D12 amendment (a dated note, the ADR's note-not-rewrite convention): an organisation administrator (a holder of organization_admin or organization_admin_no_bypass for the organisation, not a platform administrator) may create and remove sys_user_position / sys_user_permission_set rows for members of that organisation when every permission set distributed — the set itself, or each set a position carries — satisfies all three: (a) no * wildcard viewAllRecords / modifyAllRecords; (b) no adminScope; (c) every capability it carries is scope: 'org' in packages/spec/src/security/capabilities.ts. So admin_full_access, platform sets, organization_admin itself (setup.access is platform-scoped) and every delegated set are outside the boundary; organisation-administrator status is conferred only by membership grade (owner / admin → auto-grant), never copied by assignment. D12's three kinds of administrator become tenant-level, organisation-level, delegated; strict containment and the everyone / guest anchors unchanged.
  2. ADR-0131 D3 note: one dated line under D3 pointing at the D12 boundary as the rule D3's sentence runs under.
  3. The migration entry's replacement text stays as published (it becomes true); no packages/spec key.

Done when

  • Both notes are in; check-adr-anchors exit 0; Clause-②: no (ADR text only).
  • The execution card for the code (filed in the same act, Blocked-by: this card) can start.

Related: #23960 (the ruling), ADR-0090 D12 and its 2026-10-15 amendment (6181988435 on #23488), ADR-0105 D4 / D8, objectui#12082 / PR objectstack-ai/objectui#12685 (the console reads the grants).

Activity

  1. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 (takeover shift, serial) · 2026-10-17T08:56Z · #24135: ADR-0090 D12 names the organisation administrator's assignment boundary (wall-bounded sets only) and ADR-0131 D3 notes it — ruling B on #23960 (ruling 6228728551, the maintainer's 「同意」)
    Session: session_01HSDGREvqxQUt7K7pjHXMyC (the seat; the dev is an os-dev subagent of this session)
    Account: huangyiirene (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24135-adr-0090-d12-org-admin-boundary
    Worktree: objectstack-issue-24135
    Domain: domain:spec (filed by the director seat; ADR text on the protocol's permission model)
    Seat: domain:spec#2 (seat post #18549)
    File surface (paths on origin/main eda6f14b7b or later; stop on a breach and explain it in the report):


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24135,
      "status": "done",
      "branch": "claude/issue-24135-adr-0090-d12-org-admin-boundary",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24163",
      "session": "session_01HSDGREvqxQUt7K7pjHXMyC",
      "premise_still_valid": true,
      "summary": "files_changed: docs/adr/0090-permission-model-v2-concept-convergence.md (+62: one status-line entry; one dated 'Amended 2026-10-17' blockquote at the end of D12), docs/adr/0131-total-organization-ownership-no-null-organization-id.md (+6: one dated Note blockquote under D3's assignment paragraph). The D12 note states ruling B as a rule: three kinds of administrator (tenant-level = the platform administrator alone, with the ADR-0095 TENANT_ADMIN name clash called out; organization-level = organization_admin / organization_admin_no_bypass holder for one organization; delegated = adminScope holder); the organization-level kind creates and removes sys_user_position / sys_user_permission_set rows for its own members when every distributed set meets (a) no objects['*'] viewAllRecords/modifyAllRecords, (b) no adminScope, (c) only scope 'org' capabilities (PlatformCapability.scope, capabilities.ts:33; CapabilityDeclarationSchema.scope default 'platform'; an undeclared name fails); admin_full_access, platform sets, organization_admin in both variants and every delegated set stay outside; status by membership grade only; create and remove only; strict containment and the everyone/guest anchors unchanged. A4 measured on eda6f14b7b: all eight shipped sets are settled by the three conditions (inside: member_default, viewer_readonly, mcp_agent_data_read, mcp_agent_data_write, mcp_agent_restricted; outside: admin_full_access (a,c), organization_admin (a,c), organization_admin_no_bypass (c: setup.access)), so ruling item 4's denylist names no set; the table is in the PR body. Two measured precisions, both narrowing, are flagged in the PR body for the contract review: 'each set a position carries' includes the resolver's position-name fold (security-plugin.ts:6542), and the three D10 agent ceilings classify inside. No code, no packages/spec key, migration replacement text untouched. PR is draft, Tier H, assignee huangyiirene, labels skip-changeset (plus platform-applied documentation, size/s).",
      "tests": "No test applies: the diff is ADR prose only and changes no tested behaviour, so there is no ablation or reverse verification. Control-byte self-scan over both ADR files (grep -naP for the C0/DEL range) found nothing (grep exit 1); check:nul-bytes exit 0 on the final head.",
      "gates": "Final head b0173e29c, derivation 'dispatch-gates --commands --repo objectstack-ai/objectstack' (no paths; 19 commands; identical list on 44aab1b75 and b0173e29c). Each run with exit captured before any pipe: check-adr-links :: 0 ('769 relative link destination(s) under docs/adr/ resolve'); check-adr-links --self-test :: 0; check-adr-symbol-anchors :: 0 ('2353 anchors across 142 records resolve'); --self-test :: 0; check-ci-filter-parity :: 0; check-closing-keyword-parity :: 0; --self-test :: 0; check-comment-mask-corpus :: 0 ('9404 files, 0 disagree'); pnpm --filter @objectstack/lint run check:doc-formula-expressions :: 0 (first run exit 3 PREREQUISITE NOT MET, formula and lint unbuilt; built under os-verify-lock 'pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2', VERDICT command-exit 0, 4 tasks; re-run 0); pnpm check:adr-anchors :: 0 ('check-adr-anchors: OK (70 anchored file(s) ... 47869 citation(s) across 6347 file(s) resolve ...)'); check:cross-package-test-inputs :: 0; check:doc-authoring :: 0; check:driver-memory-census :: 0; check:gitlink-declared :: 0; check:nul-bytes :: 0; check:pm-governed-merges :: 0; check:pm-prior-rulings :: 0; check:refd-timer-probe :: 0; check:watch-hint-literal :: 0. --ran: '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)'. check-governed-merges --branch: exit 3, 'GOVERNED, a human merge is the review record for this PR', 'landing tier: H', docs/adr/** x2, under the 3000-line threshold. CI check-runs on b0173e29c, read once at report time (40 runs): success: Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint PM dispatch-gates, Lint scoped slot lookup and corpus, the three claim/closing guards, filter; skipped by selector: Build Core, Build Docs, Console Pin Gate, Temporal Conformance, Packed-tarball smoke, Dogfood shards, Dogfood Verify CLI; in_progress: Check Changeset, Lint (CI release and engine, ESLint, docs and code guards, repo and tooling, query-options erasure, tooling self-tests), Test Core 1-6, Type Check (consumer gates, debt ledger, source gates, workspace). Not polled further, per contract. test:repo and the local-only families (check:dev-prereqs, check:skill-frame-freshness) not run locally: no package and no packages/spec or frame file is touched.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "4 REST writes, all through the fleet-write relay as objectstack-fleet[bot], carried by 3 POST /repos/objectstack-ai/objectstack/dispatches strokes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 24163; read-back identical, 12768 bytes) and POST /repos/objectstack-ai/objectstack/issues/24163/assignees (huangyiirene) in one pr_create stroke; POST /repos/objectstack-ai/objectstack/issues/24163/labels (skip-changeset, via label-write.mjs, read-back matches); POST /repos/objectstack-ai/objectstack/issues/24135/comments (this report, via post-stamped.mjs). Plus git push x3 to the branch (not REST). Reads: single-card REST GETs of issue 24135 and its comments, comments 6228728551 / 6228146260 / 6228658787, PR 24163 and its head's check-runs.",
      "deviations": "(1) check:doc-formula-expressions first answered exit 3 (unbuilt prerequisites in the fresh worktree); rebuilt under the lock and re-ran green; recorded, not a failed measurement. (2) The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; the commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body the session-URL footer, per AGENTS.md. (3) No status-line entry was added to ADR-0131: the claim's file surface names one dated note under D3, and ADR-0131's own 2026-10-10 inline withdrawal set the note-without-status-entry precedent. (4) No environment variable value was printed.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: #24136 · noted, not filed — read-only inference: the delegated gate's position walk (setsBoundToPosition, delegated-admin-gate.ts:1086) reads only the definition's permissionSets, while the resolver also grants the permission set of the position's own name (security-plugin.ts:6542), so a delegate's allowlist check does not see a same-named set; #24136's third branch reuses that walk. Dedupe words: position name fold, setsBoundToPosition, same-named permission set, delegated allowlist",
        "carrier: #24136 · noted, not filed — isTenantAdmin (delegated-admin-gate.ts:256, wildcard modifyAllRecords) reads the walled organization_admin as tenant-level and the wall-less variant as no administrator; under the amended D12 both are organization-level. Dedupe words: isTenantAdmin, organization_admin_no_bypass, tenant-level, organization-level",
        "carrier: #24136 · noted, not filed — docs/design/permission-model.md section 10 (the ADR's companion reference) describes delegated administration only; the organization-level kind belongs there once #24136 enforces it. Dedupe words: permission-model.md, organization-level administrator, companion reference",
        "carrier: none (承接者:无) · Acceptance notes only — read-only inference, not measured: the D10 agent ceilings carry no row-level security by design, so held as a person's direct grant their wildcard read on the Layer-0-inert SCIM projection tables is filtered only by another held set's row scope (member_default supplies it); a composition with no platform baseline is the case to measure, and the same holds for any authored wildcard-read set. Dedupe words: mcp_agent_data_read, SCIM projection row scope, fallbackPermissionSet null, wildcard read without carve-out"
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24135,
      "status": "done",
      "branch": "claude/issue-24135-adr-0090-d12-org-admin-boundary",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24163",
      "session": "session_01HSDGREvqxQUt7K7pjHXMyC",
      "premise_still_valid": true,
      "summary": "Patch round 1 on contract review 6232429140 (FAIL on b0173e29c), plus the addendum citing #24164. New head 61c8a5d34 (commits 88208b958, 61c8a5d34 on top of b0173e29c). files_changed vs base eda6f14b7b: docs/adr/0090-permission-model-v2-concept-convergence.md (+70 / -0 net), docs/adr/0131-total-organization-ownership-no-null-organization-id.md (+6 / -0, unchanged this round; it inherits the denylist by reference). This round, ADR-0090 D12 note only: (1) the measured paragraph now reads 'the three conditions settle five' and moves mcp_agent_data_read, mcp_agent_data_write and, by kind, mcp_agent_restricted from Inside to 'Outside (denylist)', 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, read every organization's rows there, a read the granting organization administrator does not hold; (2) 'names no set at this date' is gone: the denylist 'names those three sets', and a later shipped set the conditions cannot settle still joins it by amendment; (3) one sentence holds the general case (an authored set with a '*' read and no row scope on a Layer-0-inert readable object, no platform baseline) for the maintainer's decision on #24164, with no fourth condition written; (4) 'refuses from every non-system writer' became 'refuses from every writer' (reserved_identity_position takes no provenance exemption, sys-user-position.object.ts:225-265). The rule paragraph (conditions, by-name exclusions) and the status line are unchanged. Premises re-measured before editing: mcp_agent_data_read is '*' allowRead 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 to [] (app-default-permission-set.ts:40-57); #24164 exists, open, needs-user-decision. PR stays draft; body NOT patched by me: the full replacement is in pr_body_replacement (updated summary, a 'Patch round 1' section, A4 line, classification table with the three ceilings 'outside (denylist)', precision 5 rewritten, gates on 61c8a5d34, acceptance note 4 rewritten, 维护者速读 updated with 席位意见 left empty).",
      "tests": "No test applies: ADR prose only, no tested behaviour changed, so no ablation or reverse verification. Control-byte self-scan over the ADR file (grep -naP for the C0/DEL range) found nothing (grep exit 1); check:nul-bytes exit 0 on 61c8a5d34.",
      "gates": "Final head 61c8a5d34 (also run on the intermediate head 88208b958, all 19 exit 0). 'dispatch-gates --commands --repo objectstack-ai/objectstack' (no paths): 19 commands, the same 19 as on every earlier head; change set the two ADR files, +76 / -0. Each run on 61c8a5d34 with exit captured before any pipe: check-adr-links :: 0 ('769 relative link destination(s) under docs/adr/ resolve'); --self-test :: 0; check-adr-symbol-anchors :: 0 ('2353 anchors across 142 records resolve'); --self-test :: 0; check-ci-filter-parity :: 0; check-closing-keyword-parity :: 0; --self-test :: 0; check-comment-mask-corpus :: 0 ('9404 files, 0 disagree'); pnpm --filter @objectstack/lint run check:doc-formula-expressions :: 0 (prerequisites rebuilt first in the re-created worktree under os-verify-lock, VERDICT command-exit 0, 4 of 4 turbo tasks cached); pnpm check:adr-anchors :: 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 ...)'); 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 :: 0 each. --ran: '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)'. check-governed-merges --branch: exit 3, 'GOVERNED, a human merge is the review record for this PR', 'landing tier: H', merge-base eda6f14b7b (origin/main unchanged, no merge needed). CI check-runs on 61c8a5d34, read once at report time (38): 29 success (incl. Lint & Repo Gates, TypeScript Type Check, Governed Surface Queue Guard, Test Core 2-6, all Lint and Type Check lanes, Check Documentation Links, the claim/closing guards); 8 skipped by selector (Build Core, Build Docs, Check Changeset (the skipped instance; no second instance listed at read time), Console Pin Gate, Dogfood shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance); 1 in_progress (Test Core 1/6). Not polled further.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "1 REST write this round: POST /repos/objectstack-ai/objectstack/issues/24135/comments (this report, via post-stamped.mjs through the fleet-write relay, one POST /repos/objectstack-ai/objectstack/dispatches stroke, as objectstack-fleet[bot]). Plus git push x2 (88208b958, 61c8a5d34; not REST). No PR body PATCH, no label write, no draft flip. Reads: single-card REST GETs of review comment 6232429140, issue 24164, PR 24163 and its head's check-runs.",
      "deviations": "(1) The worktree had been removed after round 0; it was re-created from the pushed branch tip (b0173e29c, equal to origin), never from zero, then pnpm install --frozen-lockfile (exit 0). (2) The ceilings' reason sentence names 'the two reading ceilings' for the no-row-scope read, since mcp_agent_restricted (objects: {}) reads nothing and is denylisted by kind, as the order asked. (3) The rule paragraph's by-name exclusion list was not extended to point at the denylist (the order said exactly this; the denylist sits in the measured paragraph of the same note). Option for the seat: append ', and every set ruling item 4's denylist below names' to that list if #24136's implementer should meet it in the rule itself. (4) The PR body is not patched; pr_body_replacement carries the full text for the seat to write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: #24136 · noted, not filed — read-only inference: the delegated gate's position walk (setsBoundToPosition, delegated-admin-gate.ts:1086) reads only the definition's permissionSets, while the resolver also grants the permission set of the position's own name (security-plugin.ts:6542); the ADR text now obliges #24136's third branch to count it. Dedupe words: position name fold, setsBoundToPosition, same-named permission set, delegated allowlist",
        "carrier: #24136 · noted, not filed — isTenantAdmin (delegated-admin-gate.ts:256, wildcard modifyAllRecords) reads the walled organization_admin as tenant-level and the wall-less variant as no administrator; under the amended D12 both are organization-level. Dedupe words: isTenantAdmin, organization_admin_no_bypass, tenant-level, organization-level",
        "carrier: #24136 · noted, not filed — docs/design/permission-model.md section 10 describes delegated administration only; the organization-level kind belongs there once #24136 enforces it. Dedupe words: permission-model.md, organization-level administrator, companion reference",
        "carrier: #24164 · 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 there; the shipped instance (the three D10 agent ceilings) is on the item-4 denylist in this PR. Replaces round 0's 'carrier: none' note, which the review refused. Dedupe words: mcp_agent_data_read, SCIM projection row scope, fallbackPermissionSet null, wildcard read without carve-out"
      ],
      "pr_body_replacement": "Fixes #24135\nClause-②: no\n\n## Summary\n\nExecutes 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.\n\n- **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.\n- **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.\n- 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.\n\n## Patch round 1 (contract review `6232429140` on head `b0173e29c`, FAIL)\n\nThe 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:\n\n1. 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;\n2. 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;\n3. 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;\n4. 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`).\n\nThe 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.\n\n## Mechanism assumptions, measured on `origin/main` `eda6f14b7b`\n\n- **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`).\n- **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).\n- **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.\n- **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).\n- **A5 — holds.** No heading was added (both notes are blockquotes), so no anchor moved; `pnpm check:adr-anchors` exits 0 on the final head.\n\n### Shipped permission sets against the three conditions\n\n| set | (a) no `*` superuser bit | (b) no `adminScope` | (c) `org` capabilities only | verdict |\n|---|---|---|---|---|\n| `admin_full_access` | no — `viewAllRecords` + `modifyAllRecords` | yes | no — `manage_users`, `manage_metadata`, `manage_platform_settings`, `setup.access`, `studio.access` | **outside** |\n| `organization_admin` | no — `viewAllRecords` + `modifyAllRecords` | yes | no — `setup.access` | **outside** (also by name) |\n| `organization_admin_no_bypass` | yes — the derivation drops both bits | yes | no — `setup.access` | **outside** (also by name) |\n| `member_default` | yes — no `*` entry at all | yes | yes — none | inside |\n| `viewer_readonly` | yes — `allowRead` only | yes | yes — none | inside (it carries the SCIM row scope itself) |\n| `mcp_agent_data_read` | yes — `allowRead` only, no row scope | yes | yes — none | **outside (denylist)** — mis-settled |\n| `mcp_agent_data_write` | yes — CRUD bits only, no row scope | yes | yes — none | **outside (denylist)** — mis-settled |\n| `mcp_agent_restricted` | yes — `objects: {}` | yes | yes — none | **outside (denylist)** — by kind |\n\nShipped 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.\n\n## Precisions the contract review should read\n\nEach narrows; none widens the ruled rule.\n\n1. **\"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.\n2. **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.\n3. **\"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.\n4. **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.\n5. **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.\n6. **Create and remove only** — the ruling's `allowCreate` / `allowDelete` on, `allowEdit` off.\n\n## Gates (final head `61c8a5d34`)\n\n`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:\n\n- `node scripts/check-adr-links.mjs` :: exit 0 — \"769 relative link destination(s) under docs/adr/ resolve\"; `--self-test` :: exit 0\n- `node scripts/check-adr-symbol-anchors.mjs` :: exit 0 — \"2353 anchors across 142 records resolve\"; `--self-test` :: exit 0\n- `node scripts/check-ci-filter-parity.mjs` :: exit 0\n- `node scripts/check-closing-keyword-parity.mjs` :: exit 0; `--self-test` :: exit 0\n- `node scripts/check-comment-mask-corpus.mjs` :: exit 0 — \"9404 files, 0 disagree\"\n- `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)\n- `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 …)\"\n- `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\n\n`--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)\".\n\n`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.\n\nEverything 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.\n\n## Acceptance notes\n\nRead-only inferences from the measurement above, not reproduced and not filed:\n\n- 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.\n- `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.\n- 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.\n- 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.\n\n## 维护者速读(草稿)\n\n### 改了什么\n\n在权限模型 ADR-0090 的 D12(管理权限)里加了一条带日期的修订,并在 ADR-0131 D3 下加了一行注记,纯文字,不改代码、不加 spec 键。修订把管理者分成三类:租户级(平台管理员)、组织级(组织的 owner / admin,自动获得组织管理员权限集)、委托级(带 `adminScope` 的人)。组织级管理员可以给**本组织成员**增加或移除岗位与权限集,但只限\"被组织墙约束住\"的集:不带通配超级用户位、不带 `adminScope`、所含能力全部是组织级。`admin_full_access`、平台级集、组织管理员集本身、任何委托集都不能分;组织管理员身份只能由成员资格(owner / admin)获得,不能靠分配复制。本轮按席位复核意见补了一条:三个给 AI 代理用的 MCP 上限权限集列入裁决预留的黑名单,组织管理员不能把它们分给人。\n\n### 为什么改\n\n这是您 10 月 17 日对 #23960 的裁决 B(「同意」)的第一步。ADR-0131 D3 承诺\"组织的管理员给本组织成员分配\",但代码为了防止组织管理员给自己绑 `admin_full_access` 越过组织墙,把两张分配表设成了只读;缺的正是\"组织管理员能分配哪些\"这条边界。边界先写进 ADR,代码卡 #24136(被本卡阻塞)才能开工。实测出货的 8 个权限集:三条件能判定 5 个(2 个在内、3 个在外);剩下 3 个 MCP 代理上限集按三条件看似在内,但它们本来只配给 AI 代理、靠委托人自己的权限收窄,直接分给人时在\"不配平台基线\"的部署里能读到其他组织的 SCIM 身份数据,而分配它的组织管理员自己都读不到——所以按裁决第 4 条列入黑名单。\n\n### 风险与代价(含回滚)\n\n只改两份 ADR 的文字,今天的产品行为不变:两张分配表仍然只读,直到 #24136 落地。规则措辞上的几处精确化全部是收紧、没有放宽:未声明范围的能力按平台级算;岗位按名字带出的同名权限集也算\"岗位携带的集\";无墙部署用的 `organization_admin_no_bypass` 也按名字排除;三个 MCP 上限集列入黑名单。还有一个更一般的口子没有在本 ADR 里定:任何自建的\"通配只读、没有行级范围\"的权限集,在不配平台基线的部署里同样能碰到那几张没有组织列的 SCIM 表——是否加第四个条件,还是把保证写成\"以平台基线为前提\",已单独立成待您决策的 #24164,本 PR 不替您决定。回滚:直接 revert 本 PR,不涉及数据和发布。\n\n### 席位意见\n\n### 你要做的(一个动作)\n\n席位复核通过后,在本 PR 上点 Approve。\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01HSDGREvqxQUt7K7pjHXMyC)_\n"
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24163 comment 6232732585 · domain:spec seat 2 · 2026-10-17T10:20Z · head 61c8a5d34, Tier H: draft, needs-user-decision, review requested from os-zhuang and hotlong. Contract review PASS 6232718234 (after FAIL 6232429140).


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24163 → 7cda377c17 (Fixes #24135) · domain:spec seat 2 · huangyiirene · session session_01HSDGREvqxQUt7K7pjHXMyC · 2026-10-17T10:29Z

    probe reading
    PR object Tier H. An APPROVED review by os-zhuang (a GOVERNED_APPROVERS account) on the reviewed head at 2026-10-17T10:21Z; the same account flipped it ready and enqueued it. Merged 2026-10-17T10:29Z out of the merge queue; squash 7cda377c17 on parent eda6f14b7b.
    origin/main git merge-base --is-ancestor 7cda377c17 origin/main → exit 0
    merged vs reviewed head the squash tree 91d0229bbb equals git merge-tree --write-tree eda6f14b7b 61c8a5d343 (the reviewed head)
    queue branch no gh-readonly-queue/main/pr-24163-* ref remains
    mis-close scan the body's only closing keyword is Fixes #24135; this card closed completed, and no other issue closed in the same minute
    chain claim 6232009241 · report 6232264546 · contract review FAIL → PR #24163 comment 6232429140 · patch-round report 6232545368 · contract review PASS → PR #24163 comment 6232718234 · Tier H seat record → PR #24163 comment 6232732585 · pointer 6232753790

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:specpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions