Repository navigation
fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) - #22895
Conversation
… door (ADR-0131 D3) Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
… with single and assignment controls (ADR-0131 D3) Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…g refusal Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…131 D3) Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…ll-catalog-refusal
…087 semantic entry; census row 55b Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…ll-catalog-refusal # Conflicts: # content/docs/permissions/system-context.mdx
…efusal (ADR-0131 D3) Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 14 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 74a360f307087ca6031391273c8f8e1e216841db && git checkout 74a360f307087ca6031391273c8f8e1e216841db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 018b077fbf8876c0fd5f46e66dcc551a4e74c895 b1f2335b29d12e5494247d1a2bab6796da6fad8c && git checkout -B drift-repro 018b077fbf8876c0fd5f46e66dcc551a4e74c895 && git merge --no-ff b1f2335b29d12e5494247d1a2bab6796da6fad8c
node scripts/docs-audit/affected-docs.mjs --json 018b077fbf8876c0fd5f46e66dcc551a4e74c895
|
… row 55b names the clone door Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…y name the clone door Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: First record on this PR (stage W of #15204, the walled catalog refusal, ADR-0131 D3). Reviewed read-only: the PR body and its 19-file list (+1,196 / −29), the net diff of Gate verdicts on the head, read at 2026-10-11T22:36Z. 42 check-runs across two workflow rounds, every one ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong.
② Semver level
③ Boundary flagsEvery dev flag and every
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ace: exact doors, capability's unchanged accept set, the D12 delegate Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…he clone door and the Setup env-door write (ADR-0131 D3) Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
…nvironmentSets describe Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta record on this PR (stage W of #15204, the walled catalog refusal, ADR-0131 D3), after the same-form PASS 6114668919 on Gate verdicts on the head, read 2026-10-11T23:21Z. 34 check-runs: 29 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong. Items 1 to 11 of 6114668919's ① are adopted on this head: the compare lists none of the files they judge, so the predicate, the four REST doors, the dispatcher
② Semver level
③ Boundary flagsEvery dev flag and every
Implemented-by: VERDICT: PASS Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 38185050868 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Seat note: dequeued on Test Core (4/6) · being root-caused, not re-queued blind · epic PM
|
Seat note: the Test Core (4/6) dequeue is not reproduced on this PR's merge or on
|
Resolve content/docs/permissions/system-context.mdx: keep row 55b (this branch) and main's row 58 rewording (#22875); counts re-derived with pnpm gen:system-context-census. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Seat note: re-synced onto
|
Resolve content/docs/permissions/system-context.mdx: keep row 55b (this branch) and main's row 29c (#22891); counts re-derived with pnpm gen:system-context-census. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Seat note: re-synced onto
|
…or.test.ts reads the retired objects from S8-T's testkit main's #22895 landed after S8 was cut and imports the four object files S8 deletes. This adaptation exists on the C3 build-ahead integration tree only; S8 carries its own resolution when it merges main. Claude-Session: https://claude.ai/code/session_01287UbjvrigpWwNfGDmdwpS Co-authored-by: Claude <noreply@anthropic.com>
Part of #15204
Clause-②: yes (narrowing)
Stage W of #15204 (claim amendment 6112750171): the walled catalog refusal. This PR builds the refusal ADR-0131 D3 names. The C3 card's seeder and catalog-object retirement stays open on #15204 and is not part of this PR.
The ruling this executes
ADR-0131 D3, verbatim: "Shared-DB multi-tenant (
group/isolated): tenant admins cannot create or edit catalog items; the operator defines the catalog for every tenant (managed packages, Studio). Creation through Setup or the API is refused with a message naming the posture and the capability." ADR-0131 D7 adds that the catalog pages are "read-only under a wall". The server refuses whatever the UI shows. This is the refusal D3 names, not a new gate. Catalog writes stay withmanage_metadataholders undersingle(#22621 → A, unchanged). Under a wall the authority is the platform operator's (#22801 → A).Measured first, on
mainat40bc51fThe harness was a booted
examples/app-crmstack (bootStack,multiTenant: 'posture-only', which gives a real non-degraded posture). It had two organizations created overPOST /auth/organization/create. Organization A's owner was granted a set carryingmanage_metadata, scoped to organization A. That owner is not the platform operator (isPlatformAdmin: false). Organization B's member held an environment set,w_shared, scoped to organization B.isolatedgroupPUT /meta/permission/w_shared(addsmanage_users)resolveUserAuthzGrants)manage_usersmanage_usersPUT /meta/position/:nameandPUT /meta/permission/:name(create)DELETE /meta/permission/:namePUT /meta/capability/:namecapabilityis code-only)POST /security/_activation/permission/w_sharedsys_permission_set:POST /data/sys_permission_setsys_position:POST /data/sys_positionPOST /data/sys_user_permission_set(assignment)Cross-tenant write: yes. One organization's
PUT /metachanged what another organization's members resolve. The precondition is that a principal below the operator rung holdsmanage_metadata. A stockorganization_admindoes not hold it, and the anti-escalation deny indefault-permission-sets.tsrefuses that admin the catalog tables at the data door. The data door's edit of an existing environment set answered 404, because the wall hides the organization-less projection row. So the data door created environment sets but did not reach another organization's set.What changed
walledCatalogWriteVerdictin@objectstack/metadata-core, besidemetaWriteCapabilityVerdict. Undergroup/isolated, a write of aposition,permissionorcapabilityis admitted only forisSystemor thePLATFORM_ADMINposture rung. The activation gate already reads that rung for the same reason. It is derived from the platform-admin anchors, never from a held capability. Undersingle, or with notenancyservice, the predicate is inert. URL spellings are folded throughcanonicalMetaUrlType, the same foldsaveMetaItemapplies, so/meta/permissions/xis judged aspermission./metadoors ask it right after the capability gate. OnRestServerthat is save, reset, publish and rollback (walledCatalogRefusal), answering403 FORBIDDEN. On the runtime dispatcher it isPUT, answering403 PERMISSION_DENIED. The posture is read only for a catalog type. The dispatcher reads it throughreadEffectiveTenancyPosture, factored out of the activation gate unchanged. REST reads it through the existingresolveAnonymousFormTenancywiring, so the posture and the protocol come from one kernel. A tenancy service that is registered but cannot answer is a 503, never a permit.walled-catalog-data-door.ts(plugin-security), a middleware onsys_position,sys_permission_setandsys_capability. It runs inside the security middleware and before the permission-set write-through, so a refused write is never translated into a metadata save. Assignments (sys_user_position,sys_user_permission_set) are not judged.walled-catalog-tenant-writes-retired. The changeset carries FROM → TO.Why not
saveMetaItemThe amendment named "the metadata door's write path, so every door that saves through
saveMetaIteminherits it". Measured:saveMetaItemreceives no caller principal, only anactorstring. It cannot tell the operator rung from a tenant without a contract change on every caller. So the predicate sits where the caller is known: each door, after its capability gate. That is the activation gate's two-step shape (refuseUngrantedActivationAuthoring, thenrefuseUngrantedActivationWrite). It is one predicate with one sentence, and no door restates it.The clone door (#22881, stage CD)
CD landed first (
45e206e4), and this PR merged it in (d2a6934e5). The clone door creates environment permission sets, so this PR adds the same call there:permission-set-clone.tsaskswalledCatalogWriteVerdict({ type: 'permission', ... })right aftermetaWriteCapabilityVerdict, with the posture read throughreadEffectiveTenancyPosture.403 PERMISSION_DENIED, the dispatcherPUT's envelope.permission-set-clone.test.ts:isolatedandgroupare refused with nothing saved; the operator rung is admitted; asinglecontrol; the capability gate still runs first.Re-judged pins
position-write-through.test.ts: the old walled pin was "an organization administrator's create lands its organization row, and no definition". That is the behaviour D3 refuses, so it now pins the refusal. The promotion pin seeds the pre-existing row-only position with a system write: that population is feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 Q3 = A's.builtin-positions.boot.test.ts: under a wall, the golden's "control: create a position row" is now403 PERMISSION_DENIED. The assignment cases do not move.Tests (first round at
de89dd413; patch round 1 at code head26c70abf8; every later commit up to637f1269bis text only: docs, the changeset, the spec entry's strings, one.describe()string and its regenerated reference page)metadata-core17 files / 386 tests, plus 8 new predicate cases.plugin-security198 files / 4057 passed.rest278 files / 5542 passed.runtime355 files / 5831 passed.specsrc/migrations205 passed. Build and typecheck green for metadata-core, rest, runtime and plugin-security.wall-catalog-refusal.dogfood.test.ts:isolatedandgroup, plus asinglecontrol, 13 tests.walled-catalog-data-door.test.ts: realObjectQLandSecurityPlugin, 12 tests.meta-walled-catalog-refusal.test.ts: the dispatcher, 11 tests.walledCatalogRefusalwas forced to admit,restrebuilt, and the marker confirmed indist/byablation-dist-preflight. The 4/metadogfood pins went red, on both postures. The data-door and control pins stayed green, as expected. Restored to theHEADblob and rebuilt; the marker is absent fromdist/.singlecontrols stayed green. Restored to theHEADblob.dispatch-gates --ranon5354b1601: 125 derived, 124 run green, 1 not measured.check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET) because packages outside this diff are unbuilt here.if (!walled.allowed)refusal was made unreachable. Theisolatedandgroupclone refusal pins went red (2 failed, 20 passed). Restored to theHEADblob.permission-set-clone.test.tswithmeta-walled-catalog-refusal.test.ts: 33 passed. The dogfood fileswall-catalog-refusalandpermission-set-clone-door: 21 passed.check:system-context-census: OK, 129 sites (the clone door'sisSystemread moved it from 128).singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204, 6114447595).Acceptance notes
manage_metadataholder also writes the environment's objects, views and flows through/meta. D3 governs only the catalog. No card is filed, because only the catalog was measured.authorEnvironmentSetsscope can no longer author environment sets at the data door. D3 refuses tenants catalog creation, and a delegate is a tenant. The delegated assignment and binding paths are untouched.sys_position_permission_setis not judged: under ADR-0131 D3 its rows bind nothing, and it retires with the catalog objects.Generated by Claude Code