Repository navigation
fix(security)!: under a wall every environment-metadata write is the platform operator's, whatever its type (ADR-0131 D12 items 7–8, D11) - #23330
Conversation
…platform operator's (ADR-0131 D12 items 7-8) — wip The walled verdict stops special-casing the security catalog: the /meta doors (both transports), every /packages write door, the /automation definition writes and install-local ask it for every type, with no catalog pre-filter. The activation remedies that named a clone or a sibling item under a wall now name the operator. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…tadata write door; re-judge the pins that encoded tenant writes under a wall Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…— migration entry, changeset, census row 55b, tenancy and API docs; re-judge the W2/W3 catalog-free controls Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…lled-environment-metadata-writes
…it 60 s budget Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 40 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 149 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 331090103361bafd953635e533a457f0fe955534 && git checkout 331090103361bafd953635e533a457f0fe955534
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc14657ede79cd315fa7647da238a48a6ede9b88 dbe63f841ec091d52db73a5d048e4ab0b3b1a228 && git checkout -B drift-repro cc14657ede79cd315fa7647da238a48a6ede9b88 && git merge --no-ff dbe63f841ec091d52db73a5d048e4ab0b3b1a228
node scripts/docs-audit/affected-docs.mjs --json cc14657ede79cd315fa7647da238a48a6ede9b88
|
…lled-environment-metadata-writes
… is the platform operator's too (ADR-0131 D12 items 7-8) POST /meta/_migrate-stored asks walledCatalogWriteVerdict on both transports, after its manage_metadata gate and before any row is read: 403 FORBIDDEN on RestServer, 403 PERMISSION_DENIED on the dispatcher, preview and apply alike. single, the operator rung and isSystem are unchanged. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #23231 (body and all six comments: ruling 6145019463, claim 6148875683, shared-pages note 6149658350, dev reports 6154188969 and 6155532025, seat direction 6154226716), PR #23330 (body, the 31-file list, the net diff against its merge base), the head's check-runs (41 names; latest run per name: 34 success, 7 skipped, 0 other, ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named and judged:
The PR body's sentence " ② Semver level
③ Boundary flagsEvery dev flag and every
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #23231
Clause-②: no (narrowing)
Execution of ruling A on #23231 (record 6145019463): under a wall, every environment-metadata write admits only the platform operator and
isSystem. The one verdict,walledCatalogWriteVerdict, stops special-casing the security catalog, and no door pre-filters on the written type before asking it.singleis unchanged.The Clause-② value is measured: the diff adds no export, no error code and no response field. It removes one (
error.details.catalogItemson theinstall-localrefusal).check:api-surfaceis green.Step 1: measured first (BEFORE)
Harness. A booted
examples/app-crmstack (bootStack,multiTenant: 'posture-only', plusMarketplaceInstallLocalPlugin), with the same shape as W, W2 and W3:manage_metadata, granted scoped to organization A (rungTENANT_ADMIN,isPlatformAdmin: false).GET /meta.single, A holds the set unscoped (rungMEMBER).Measured on this branch at
bae5841bce, before any code edit.isolatedandgroupanswered identically.isolatedgroupsinglePUT /meta/object/:name(create)PUT /meta/view/:name(create)PUT /meta/flow/:name(create)PUT /meta/view/:nameover the operator's viewPOST /meta/view/:name/publishof the operator's draftPOST /meta/view/:name/rollbackof the operator's viewDELETE /meta/view/:name(reset) of the operator's viewPUT /meta/:type/:name, a non-catalog typemeta-walled-catalog-refusal.test.ts)POST /packages(create)PATCH /packages/:id,/enable,/disable,POST /publishpublish-draftson the operator's packagerevert,discard-draftsrollback,commits/:commitId/revertduplicate,adopt-orphansDELETE /packages/:idinstall-localinstall (a package carrying an object)install-localupgrade of the operator's packageinstall-localuninstall of the operator's packagePOST /automation(flow create; not named by the ruling)POST /meta/_migrate-stored(preview and apply; both transports; added in patch round 1, seat direction 6154226716)On the first harness run,
adopt-orphansanswered 403 under both walls. The environment then held the harness's own catalog orphan, so W3's catalog refusal fired. Once that set was saved into a package, the door answered 200; that second run is the one tabled.Verdict on the premise. No named door was already closed. One organization's write reached every other organization at every door, so the refusal is owed at all of them. The data door to the ledger itself does not exist:
POST /data/sys_metadataanswered 405.The
organization_adminassignment reading. On the same harness, organization B's stock owner (organization_admin, rungTENANT_ADMIN) postedPOST /data/sys_user_permission_set, under both walls. Every case answered 403PERMISSION_DENIED(objectsys_user_permission_set):manage_metadataset, to a member of organization B;The reason is the grant, not the capability:
organization_adminis read-only onsys_user_permission_setandsys_user_position(default-permission-sets.ts). So a stock organization admin cannot hand outmanage_metadata. An organization-scoped holder appears only through an operator's grant: an operator-authored set, a system write, or an operator-granted assignment-table grant oradminScopeallowlist. This is a reading only, and the door refusal does not depend on it.The PM's mechanism hypotheses, measured
isSecurityCatalogTypeline (meta-write-capability.ts).walledCatalogRefusalpre-filter.if (isSecurityCatalogType(type))wrapper./packageshelper andinstall-localboth asked the verdict forSECURITY_CATALOG_TYPES[0]and then refused only if the items read included a catalog type.manage_org_presentationarm. A holder ofmanage_metadataitself passed every door (table above).createWalledCatalogDataDoorRefusalmaps onlysys_position,sys_permission_setandsys_capabilityto a type, and callsnext()for every other object before asking the verdict. A wider verdict reaches no other data. It and the catalog gates insecurity-plugin.tsare not edited (seat 2's region).manage_metadatafirst:metaWriteCapabilityVerdict,requireManageMetadata,install-local'srequireInstallCapabilityand the/automationrefuseUngrantedFlowWrite.admin_full_access(ADMIN_FULL_ACCESS_CAPABILITIES).organization_admincarriesmanage_org_users,setup.access,setup.writeandmanage_mailboxes.DEFAULT_METADATA_TYPE_REGISTRY, fromobjecttoskill. Each is stored insys_metadata, which carries no organization column (ADR-0131 D6/D7)./metatype.manage_metadataholder only.group. See the fork-clause reading below.The fork clause: what the repo says HQ IT holds
manage_metadata/studio.access) is what headquarters IT holds and plant admins do not." It names the capability, not the posture rung. The same item calls every customization "environment-wide and operator-gated (D6)".posture-ladder.ts).PLATFORM_ADMINderives from the platform-admin anchors and never from a held capability. Undergroupandisolatedthe anchor isOS_PLATFORM_OWNER_EMAIL, whichenvironment-variables.mdxmarks "Required under the walled postures". Holdingmanage_metadatanever lifts the rung.delegated-administration.mdx. It says HQ is not constrained by delegation, and it does not say who holdsmanage_metadata.group. The onlygroupharnesses are the W family dogfood tests, where the platform operator is the seeded admin declared inOS_PLATFORM_OWNER_EMAIL.Reading. In a stock
groupdeployment, HQ IT'smanage_metadatacomes from the platform-admin set, so HQ IT is the operator and keeps authoring. The ADR's own text, though, describes HQ IT as amanage_metadataholder without naming the rung. A deployment that gave HQ IT an operator-authored set granted inside the HQ organization has a non-operator HQ IT. Under this PR that HQ IT is refused every metadata write, and its administrators must be declared inOS_PLATFORM_OWNER_EMAIL.That is the operational consequence the ruling asked to surface. It is reported on #23231, the PR stays a draft, and
content/docs/deployment/tenancy-modes.mdxnow says it in the deployer's terms.What changed
@objectstack/metadata-core:walledCatalogWriteVerdict.@objectstack/rest.walledCatalogRefusaldrops its pre-filter. The four/metadoors (save, reset, publish, rollback) refuse a non-operator403 FORBIDDENfor every type.@objectstack/runtime.PUT /metadrops its catalog wrapper and answers403 PERMISSION_DENIED.refuseWalledCatalogPackageWritedrops its item reading. All thirteen/packageswrite doors ask it right afterrequireManageMetadata, before any read or write. Its three item readers (storedRowTypes,readCommitRow,commitItemTypes) are deleted./automationdefinition writes (create, update, delete, clone) ask the verdict through the newrefuseWalledFlowDefinitionWrite. The enablement door keeps the ADR-0126 §5 activation gate.POST /meta/_migrate-stored(patch round 1, seat direction 6154226716):manage_metadatagate and before the protocol is resolved or any row is read:rest-server.ts, and the dispatcher'sruntime/src/domains/meta.ts.403 FORBIDDEN/403 PERMISSION_DENIED, each in its door's own envelope.@objectstack/cloud-connection.refuseWalledCatalogPackagedrops its catalog reading.install-localinstall, upgrade and uninstall refuse a non-operator403 FORBIDDEN.activation-gate.ts). The flow, action and permission-set remedies recommended a clone or a sibling item. That refusal is answered only under a wall, where those writes are the operator's now (and for permission-set clones, since W). The remedies now name the operator.@objectstack/spec.walled-environment-metadata-writes-retired.deployment/tenancy-modes.mdx: a postures-table row, and a section on who writes the environment's metadata and on HQ IT.api/metadata-api.mdx: thePUT /metaand/packagesrules.permissions/permission-sets.mdxandpermissions/capabilities.mdx.permissions/system-context.mdx, which now anchorsrefuseWalledFlowDefinitionWritetoo.meta-write-capability.ts, and the changeset.changeset/23231-walled-environment-metadata-writes.md.Re-judged pins (the repo-wide sweep)
A tenant's write under a wall flips from 2xx to 403, so every pin that encoded the 2xx was flipped to assert the refusal and what did not move:
meta-walled-catalog-refusal.test.ts: the "a type outside the catalog is not judged" control now asserts a refusal for an object, a view and a flow. It gainsisSystemand operator controls.automation-activation-posture-gate.test.ts: the refusal no longer recommends a clone. The clone door refuses the tenant under a wall, with the walled sentence, not the activation one.packages-vetted-org-source.test.ts: the members are refused at every write door before any protocol call. The read doors are unchanged. A new operator arm keeps the "no organization is threaded" property for admitted writes.packages-uninstall-refuse-before-mutate.test.ts: the three non-operator callers are refused with nothing moved. The operator, with and without an active organization, uninstalls naming no organization.packages-orgless-grants-capability-gate.test.ts: the controls show that the capability gate admitted the member, and that the walled refusal answered next. The operator is the positive control.meta-read-org-scope-parity.test.ts: both transports refuse the ex-member under the wall. The operator's save lands env-wide on both.wall-package-refusal.dogfood.test.ts(W2) andwall-package-doors-refusal.dogfood.test.ts(W3): A's catalog-free CONTROL becomes a refusal pin. A can no longer create packages, so the "other package" arms use an operator-created package.The refusal assertions of the catalog pins are kept verbatim. Message pins assert only what the message names: the posture, the ruling and the subject.
Tests
Patch round 1 (head
dbe63f841e=824228ec35+ a merge oforigin/main2868248e5e+ one commit):rest/src/meta-migrate-stored-walled-refusal.test.ts: 6 cases. Underisolatedandgroup, preview and apply are refusedFORBIDDEN/ 403, andmigrateStoredMetadatais never called. The operator,isSystem,singleand no-tenancy controls get 200, and the capability gate still answers first.runtime/src/domains/meta-walled-catalog-refusal.test.ts(19 passed).meta-write-capability.test.ts(15 passed).singlecontrols in the dogfood wall file.isolatedandgroup, naming the posture and ADR-0131; 200 undersingle.dbe63f841e:src/domains: 116 files / 2972 passed;/metaand posture files: 110 / 2284;check:platform-checklistexits 1 on the same 8 ABSENT SYMBOL problems that reproduce onorigin/main, none in this diff.All runs below are at head
824228ec35, the merge oforigin/main5c9bfb0d3f, unless noted.wall-environment-metadata-refusal.dogfood.test.ts: 21 tests overisolated,groupandsingle. It covers every door above, B's read-back, the operator controls and thesinglecontrol.packages-walled-write-refusal.test.ts: the 13 doors × 2 postures, with operator,isSystem,singleand no-tenancy controls.automation-walled-definition-write.test.ts: the 4 writes × 2 postures, with the same controls.FORBIDDENon REST/metaandinstall-local,PERMISSION_DENIEDon/packagesand/automation.singleis unchanged, cell for cell. The operator control answers 200.@objectstack/runtime, the whole package: 374 files, 6,178 passed, 19 skipped, in four runs (domains 116 files, plus 55, 100 and 103).@objectstack/rest, the whole package: 284 files, 6,049 passed, 328 skipped, in two runs.@objectstack/metadata-core: 18 files, 552 passed.@objectstack/cloud-connection: 45 files, 693 passed.@objectstack/specsrc/migrations: 4 files, 208 passed.automation-toggle-tenant-scope,federated-anchor-provenance, the fourinstall-local-*files,public-form-withdrawal-walled,schedule-acting-organization,security-catalog-showcase,showcase-public-form-redirectandshowcase-public-form-walled-intake. This run was before the merge, at733500eb11.turbo run typecheckfor metadata-core, rest, runtime, cloud-connection, spec and dogfood: 71 of 71 tasks green. Runtime'scheck:test-typecheckcovers its test files.eslint.config.mjsapplies to**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}.eslint --no-inline-config --format jsonover the 22 changed TS files gives 22 files, 0 errors and 0 warnings.parserOptions.project, per its own note), so this diff cannot move a verdict on an untouched file.ablation-replace.mjs: anchor 1 → 0, blob1fc932c81c→a6632971b2.ablation-dist-preflightfound the marker indist/index.cjsanddist/index.js.singlecontrol green.git diff HEADis empty, the rebuild shows the marker absent from all 12 dist files, and the tree is clean.walledCatalogRefusal: anchor 1 → 0, blob94317088f7→1512e81a2c. rest was rebuilt, and the marker was found indist/index.cjsanddist/index.js./metapins red (5 × 2 postures). The/packages,install-localand/automationpins and every control stayed green (11).dispatch-gates --commandsderives 122 commands;--ranaccounts for all 122, 0 not measured.check:query-options-erasuretimed out under contention and passed on a rerun.check:skill-examplesandcheck:dual-build-cjs-loadspassed once the packages they read were built.check:platform-checklistexits 1 with 8 ABSENT SYMBOL problems inaccess-security.json,attachments-storage.jsonandsearch.json. The identical 8 reproduce onorigin/main404fc477e5. This diff's checklist edit (api-backend.json) is not among them.e7fdcc48e2. It finished on824228ec35, whose only change is a test budget in the new dogfood file. The first 60 commands were rerun on824228ec35(see the report on [Decision] 墙内组织里持有 manage_metadata 的人仍能改共用的对象、视图、流程:只放行平台运营者(A)、禁止组织范围授予(B),还是维持并写明(C) #23231).Acceptance notes
packages/runtime/src/domains/automation.ts: the/automationdefinition writes. These doors were measured (200, and B reads A's flow), and leaving them open would make this PR's claim false. The bounded in-place fix holds: same defect class, the same one-call shape, no other claim on the file, the same gate family.activation-gate.ts: its remedies were made false by this change.deployment/tenancy-modes.mdx.@objectstack/cloud-connectionkeeps its@objectstack/objectqldependency, which W2 added for the catalog reading this PR removes. No import of it remains. The dependency, the lockfile entry and the vitest alias are left as they are.walledCatalogWriteVerdict, as dowalledCatalogRefusal,refuseWalledCatalogPackageWriteandrefuseWalledCatalogPackage. The security plugin's data door (seat 2's region) imports the export under that name.Generated by Claude Code