Repository navigation
refactor(plugin-security): move SecurityPlugin's Layer 0 tenant write wall into security-plugin-write-tenant-wall.ts (#25223 cut 5) - #25378
Conversation
… wall into security-plugin-write-tenant-wall.ts The body of composeWriteTenantWall (step 3.7's wall, composed once per write: the ADR-0123 D2 scope pre-test, the parts every row is held to and the payload and stored-image judgements) moves unedited to a new module beside the plugin. The method keeps its name, signature, docblock and position and hands its body one host literal of exactly the members it reads; tenancyPosture and orgScopingEnabled go as getters. The WriteTenantWall and WriteWallRefusal interfaces stay; carriesActiveOrganization stays (callerHasOrganizationScope reads it) and is exported and imported by value. The two platform-checklist citations of the body's local denyTenantPlacement are re-pointed to the new module, and the module gets the ADR-0123 D2 anchor entry. Move-only: no verdict changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
📓 Docs Drift Check17 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 16 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 bc1755b740c41bbd2c74c78264af57fafdbfae02 && git checkout bc1755b740c41bbd2c74c78264af57fafdbfae02
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fb63d396b15c91f3f16041817e9934e2dea93759 765326ddf527bc333e8fc25ce0e7f59dd9fae26b && git checkout -B drift-repro fb63d396b15c91f3f16041817e9934e2dea93759 && git merge --no-ff 765326ddf527bc333e8fc25ce0e7f59dd9fae26b
node scripts/docs-audit/affected-docs.mjs --json fb63d396b15c91f3f16041817e9934e2dea93759 |
席位核对 · ACCEPT · 2026-10-23T05:35Z
Generated by Claude Code |
Requested by Sales · project thread
Part of #25223
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6353073966
Cut 5 of the
security-plugin.tssplit: step 3.7's Layer 0 tenant write wall. The claim is 6357083478. The body of one async method,composeWriteTenantWall, moves unedited to the new modulepackages/plugins/plugin-security/src/security-plugin-write-tenant-wall.ts. It composes the wall once for one write:judgesOrganization(group posture: a member's insert whose organization_id names another organization of its membership set is silently stored in the active organization (the engine's read-only strip), where #21666 ruled an explicit value meets the wall and is never rewritten #24822).Nothing in it logs or throws. The middleware's step 3.7 logs and throws each refusal through
raiseWriteWallRefusal, and the write previewwriteWallVerdictanswers it as a row's verdict. Both stay in the class and still call the method.It is move-only, and the card stays open for the later cuts. Branch:
claude/project-thread-3fzya4, the harness-pinned branch for this session, restarted fromorigin/mainat4d7e2b1f38(cut 4's merge). It names no issue, so the claim comment is the identity record.Why now. Ruling 6353073966 item 4 held this body with the two read bodies until the Tier H docs PR for #25314 landed. That PR, #25316, merged as
97967a9a06, and cut 4 moved the two read bodies. On this base, ADR-0123:4 anchorssecurity-plugin.ts#composeWriteTenantWall, a method name. The method stays as a shell, so that anchor still resolves on its declaration.What stays in
security-plugin.ts.WriteTenantWallandWriteWallRefusalinterfaces keep their one declaration, because the method's signature andraiseWriteWallRefusalread them. The module names both through the class's own signature, as cuts 2 to 4 nameCbpRelation.carriesActiveOrganizationstays, becausecallerHasOrganizationScopereads it too. It gainsexport, and the module imports it by value, as cut 3's module imports its five values. It is read only inside the function body, at call time, never while either module evaluates.index.tsstill exports onlySecurityPlugin, so the published surface does not move (measured below).matchesFilterConditionname leaves the@objectstack/formulaimport ofsecurity-plugin.ts, because no code left there reads it. The module imports it from the same package. The module also importspostureUsesUnionScopefrom@objectstack/spec/securityandPermissionDeniedErrorfromerrors.js, assecurity-plugin.tsdoes.One reader outside the package moves with the body.
docs/qa/platform-checklist/areas/access-security.jsoncitespackages/plugins/plugin-security/src/security-plugin.ts#denyTenantPlacementtwice, in thefixtures.requiresprose of itemsaccess-security.activation-write-operator-gateandaccess-security.walled-environment-metadata-operator-only.denyTenantPlacementis a localconstinside the moved body. With the body moved and that citation unchanged,check-platform-checklistexits 1 with ABSENT SYMBOL, measured on a trial before the commit. Both citations now namesecurity-plugin-write-tenant-wall.ts#denyTenantPlacement. Only the path changed: the symbol, the quoted 403 text and the rest of both strings are as they were. The gate exits 0 at head.The published
.d.tsdoes not changeUnder the series bar (ruling 6348891160 item 1), measured on base
4d7e2b1f38and on the head (the tree of this PR's commit):dist/index.d.tsanddist/index.d.mts, base and head, all four the same:ea54b6c9538b135fee36e1309fddc763113443c0dddd174950cbd96c290536a2. The raw diff is 0 lines, so no union order moved either.652f26b24f41bf031d428f708685abb7517d4247a765246d153d2bbeac13e444.origin/mainmoved between cut 4's head and its merge. The only lines that differ from cut 4's head.d.tsare six inlinedrequiresFeatureunions from@objectstack/spec, which gained"cloudManaged". Base and head here are both built against4d7e2b1f38's dependencies.packages/plugins/plugin-security(it sorts the members of each string-literal union and changes nothing else):dist/index.d.ts, line 1794 (private composeWriteTenantWall;):composeWriteTenantWall;): raw5c1fa0e3230960f533466bb151a1e1769cdcc855d3227f776a9832d6f0fb7195, normalised4defa680f3cf4b2ed2b0c60696d8bea7031a82edd43bcabc4ad4b2e346727c80.private composeWriteTenantWallRenamed;): raw82704c4f85cb5d28cb672046ef7e2b05da8348c6cb4efccd4e0e5fe7e58eb0dc, normalised614cf491f999aa7a4fde4a3317cec1b1ee9e95306e29dab7c5f1c452b9982870.requireandimport()of the built bundles), not parsed. The sorted name list hashesf607fd765d6b277e08b3b46ccb16d1e2d133ec55658ffe40155aef25e1bf54f0on all four, the same as at cuts 2 to 4. The newcarriesActiveOrganizationexport ofsecurity-plugin.tsdoes not reach the entry.index.js53af1376toa0e03516,index.mjs7beda6f7toa75436eb), because code moved within them.No changeset:
skip-changeset, per ruling 6348891160 item 4.What moved
The base line numbers are the method's signature and closing brace at
4d7e2b1f38. The last column lists the members the body reads, which is exactly what its host carries.composeWriteTenantWall(async):8408-:8746computeWriteTenantCheckFilter,computeWriteTenantWall,orgScopingEnabled,organizationWallRefusal,tenancyPostureThe stub drops
asyncand returns the moved function's promise, as cuts 1 and 3 did for their async methods (6 and 5 of them). The.d.tsemits a private member without modifiers, so this does not reach it.The shape (#23050 shape B, as cuts 1 to 4 applied it)
.d.ts. The body is now one host literal pluscomposeWriteTenantWall.call(host, …), marked[#25223 cut 5].WriteTenantWallHosttypes every member as the class member itself (SecurityPlugin['m']). The moved function types itsthisas thePickof the host over exactly the members its body reads, and the stub annotates its literal with the samePick. There is no cast, and no member becomes public or protected.SecurityPlugintype-only. It namesWriteTenantWallas the awaited return type ofSecurityPlugin['composeWriteTenantWall'], andWriteWallRefusalas the non-null type of that wall'spreTest.this.m(...a)on the plugin at call time. A spy or test double set on the plugin is still the one the body reaches. The pre-test is stillorganizationWallRefusal, the one verdictcanWriteObjectasks too. The caller's and the delegator's Layer 0 still come from the read side'scomputeLayeredRlsFilter, throughcomputeWriteTenantWallandcomputeWriteTenantCheckFilter. No moved body calls a function-valued field, so no receiver changes.tenancyPostureis not declaredreadonly, becausestart()resolves it after construction.orgScopingEnabledis the class's own getter over it, handed over as cut 1 handed it. The host declares bothreadonly, so the moved body cannot assign a member through it; the typecheck passes, so it does not.this.tenancyPosturehappen when a returned judgement is called, not when the wall is composed: the developer message ofdenyOrganizationRemoval, whichjudgePayloadandjudgeRowreach, and the developer messages ofjudgeLanding's two refusals.thisinside those arrow functions is the host, and the host's getter reads the plugin's field at that call, which is the read the method made before.Proof of move-only
asyncmatch. A negative control,rows.some(emptiesOrganization)changed to!rows.some(emptiesOrganization)in the payload half, fails the check.async. Every other class member, 124 named members plus the constructor, is byte-identical to base,organizationWallRefusal,computeWriteTenantWall,computeWriteTenantCheckFilter,raiseWriteWallRefusal,writeWallVerdictandstart()included. The class has 126 members at base and at head.WriteTenantWallandWriteWallRefusalincluded. The two that differ are the@objectstack/formulaimport, which losesmatchesFilterCondition, andcarriesActiveOrganization, which is the base text withexportbeforefunction. The one new statement is the import of the new module.git diff --color-moved=plain --color-moved-ws=allow-indentation-change 4d7e2b1f38 HEADover the two source files): 328 of 331 deleted lines and 337 of 453 added lines are detected as moved.matchesFilterCondition,import line, thefunction carriesActiveOrganization(line, which comes back withexport, and the method's signature line, which comes back withoutasync.WriteTenantWallHost, the pointer docblock and the rewritten signature head. The rest are lines git does not pair with a moved block, such as blanks and closing braces.security-plugin.tsgoes from 10,205 to 9,890 lines.ADR anchors (ruling 6353073966 item 2)
node scripts/check-adr-symbol-anchors.mjs: exit 0 at base4d7e2b1f38and exit 0 at head, both with2383 anchors across 142 records resolve — 466 symbol (440 declaration, 26 literal), 1889 file-level, 28 cross-repo, 6 exempt, 18 continuation, the same counts as cut 4. CI runs this gate (lint.yml), butdispatch-gates --commandsdoes not derive it for this change set ([finding] dispatch-gates derives the symbol-anchor sweeps only from their corpus paths, so a change that removes an anchored symbol from a TARGET file passes every local gate and reds CI #21697), so it is run by hand here.pnpm check:adr-anchors: exit 0, 79 anchored files (78 at base, plus the new entry). The new entry isscripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-write-tenant-wall.ts.json. It carries ADR-0123 and the ADR-0123 D2 paragraph of thesecurity-plugin.tsentry, verbatim: the no-active-organization refusal is this body's pre-test, and the paragraph's rule that it stays ordered ahead of the forge guard is about code that now lives in this module. The moved comments also cite ADR-0090, ADR-0095, ADR-0105, ADR-0112 and ADR-0131. Like thesecurity-plugin.tsentry, the new entry does not list every ADR its file cites, and the gate passes. Thesecurity-plugin.tsentry is unchanged: the middleware's step 3.7, which throws that refusal, stays there.docs/adr/**is not touched.security-plugin.ts: 27 anchors, 18 symbols, 10 ADRs on this base. It was measured withextractAnchorsandsymbolSegmentResolutionfromscripts/symbol-anchors.mjs, which is measurement, not a new gate. It was read on4d7e2b1f38BEFORE the cut, with the body blanked: no anchor changed class. Every anchor resolves at head insecurity-plugin.tswith its base class.The table gives each anchor's class at base and head, the line it resolves on, and the member that line sits in. Every anchor stays in
security-plugin.ts.#CBP_MAX_CHAIN_DEPTH:657(top-level):660#SecurityPlugin:1334(class):1337#SecurityPlugin.getMetadataReadableFields:2483(start),:6102(method):2486,:6105#SecurityPlugin.resolvePermissionSetsForContextUnmemoized:7013(method):7016#assertControlledByParentWrite:9462(method shell):9147#assertMasterRowEditable:9588(method shell):9273#collectRLSPolicies:9642(method):9327#composeWriteTenantWall:8408(method):8411(method shell)#computeControlledByParentFilter:9054(method shell):8739#computeDelegatedReadFilter:5980(method):5983#computeRlsFilter:5232(explainAccessForCaller),:7635(method):5235,:7638#getReadFilter:2246(start),:5858(method):2249,:5861#hasPlatformAdminPosture:7662(method):7665#isPrincipalLessContext:511(top-level):514#manage_metadata:260(PLATFORM_ADMIN_ONLY_CAPABILITIES):263#nav_permission_sets:1739(init):1742#rlsFilter:4309(local instart):4312#writeCheckPolicies:1229(top-level):1232No resolving site moves. ADR-0123:4 resolves on the
composeWriteTenantWalldeclaration, which stays as a shell. The new module also declares a function of that name, but the anchor namessecurity-plugin.ts, so it is judged there. This cut adds no host-literal key line for an anchored name.Readers
One reader of the moved lines exists outside this package, the platform checklist above, and it is re-pointed in this PR. No test, script, gate, baseline or other docs anchor reads the moved lines by path.
check-platform-checklistexits 0 at base and at head. Both printsymbol anchors: 986/996 resolved … 10 on the named #16898 residual, 18 file floors held. The cited-source count goes from 435 to 436, because the new module is now a cited source. The scoped run--changed-from origin/mainjudged 7 anchors in 3 anchored files of the change set, and lost 0. Before the re-point, on a trial of the same move, the full gate exited 1 with ABSENT SYMBOL for both citations.isSystemread. One comment namesisSystem, which the census does not count. Socheck-system-context-censushas no row to move, andcontent/docs/permissions/system-context.mdxis not touched.node scripts/isystem-census.mjs --jsongives the same 148 sites at base and head (12 insecurity-plugin*.ts), compared without line numbers. Only the corpus and scanned file counts move, by one. The body holds no engine write, sonode scripts/tenant-audit-census.mjs --jsongives the same 237 write call sites at base and head, compared the same way. OnlyscannedSourcesmoves, 712 to 713, plus the measured-at ref.check-scripts-symbol-anchors,check-spec-docblock-symbol-anchorsandpnpm --filter @objectstack/spec check:liveness.packages/core/src/security/row-serving-door-exposure.pin.test.tsclassifies every caller of the read admission, and the body calls no admission, so the new file needs no row. Run on its own: 3 of 3 pass.delegated-organization-membership.test.tscountsresolveDelegatorContext(per source file, and the body has none, so the two counted files are stillexplain-engine.tsandsecurity-plugin.ts. It is part of the plugin-security suite, which passes.security-plugin.tsitself name{@link SecurityPlugin.composeWriteTenantWall}(onWriteWallRefusalandWriteTenantWall, and in step 3.7 andwriteWallVerdict). The method still exists under that name.[#24784],[#24821]and[#24822]arms, and assert behaviour, not location. The suite passes at head with the same counts as base.security-plugin.ts:Ncitation in the repo points into the moved range, base:8417-:8745.:146-:186(the new import), by +3 for base:188-:8416, and by -315 after:8745. Lines:1-:145do not move. The moved range becomes the 11-line stub at head:8420-:8430.:132,:303,:309,:1614,:1690,:1736,:3215,:3281,:3519and:4344are the same list cuts 2 to 4 read. Each lands in an unmoved or shifted range, and each already pointed at an unrelated line before cut 2, so this cut does not make any of them wrong.node scripts/docs-audit/affected-docs.mjs --json 4d7e2b1f38(advisory): 0 pages. Its 17 anchors are the moved names and literals, and the one broad literal,organization_id(37 pages), is dropped as over-broad.4d7e2b1f38and head and compared:check-platform-object-tenancy-census,check-registry-log-declared,check-rest-log-spy-declared,check-system-context-census,check-tenant-audit-census,check-adr-symbol-anchors,check:cross-package-test-inputs,check:driver-memory-censusandcheck:query-options-erasure. The last reports the same counts at both, andsecurity-plugin.tsis not a baseline key.check-undeclared-dep-imports: 3298 to 3299 files, and 2837 to 2840 specifiers. The three are the new module's@objectstack/spec/security(type and value) and@objectstack/formulaimports, both declared dependencies.check:doc-authoring: 1417 to 1418 sources.check:org-identifier: 3640 to 3641 files.check:logger-receiver-detach: 3614 to 3616 files.check-position-name-fold-loaders: 12316 to 12321 files, with the same 8 references (6 test, 2 declared instrument, 0 loader). It walks the working tree. Two of the five are this PR's new files. The other three are gitignored local build outputs that exist only in the head worktree (packages/spec/json-schema/data/QueryPageWindow.json,QueryPageWindowParam.jsonandapps/docs/next-env.d.ts), which CI's checkout does not have. The second extra file ofcheck:logger-receiver-detachis the same kind of local output, as at cuts 2 to 4.check-issue-citationsjudges a change set, so it has nothing to read at base. At head it judged 19 citations across 2 files, and all resolve.Acceptance notes
docs/adr/**. The one file outside the package is the checklist, and only its two path strings change.security-plugin.tswith one import after:47and one block at:1720. docs(qa): re-author access-security.platform-owner-email-anchor for the Default Organization grant #25323 (draft) touchesaccess-security.jsonat:2919-:3063and adds no#denyTenantPlacementcitation. This cut has no hunk near either.origin/maingained only refactor(plugin-auth): move AuthManager's password, auth-gate and session policy into auth-manager-password-session-policy.ts (#25224 cut 3) #25362 (plugin-auth). It touches none of this cut's files and no reader of them, so the merge with this head is clean.security-plugin.tsafter this cut:computeWriteTenantCheckFilter,computeWriteTenantWall,organizationWallRefusal,raiseWriteWallRefusal,writeWallVerdict, the two interfaces and the middleware's step 3.7 that calls the method.Gates
All gates ran at the head, the tree of
765326ddf5, on base4d7e2b1f38.pnpm --filter @objectstack/plugin-security test(vitest run --maxWorkers=3, under the verify lock): Test Files 212 passed (212), Tests 4442 passed, 45 skipped (4487). Base4d7e2b1f38gives the same counts. VERDICT command-exit 0 on both.pnpm --filter @objectstack/plugin-security typecheck: exit 0. It runstsc --noEmit, the scripts tsconfig, andcheck:test-typecheck.eslint --no-inline-configover the two touched source files: exit 0.pnpm --filter @objectstack/plugin-security build: exit 0. It produced the.d.tsand bundles measured above. The package's dependencies were rebuilt on4d7e2b1f38first: on the dependency outputs of the previous base, the typecheck and build failed on two@objectstack/coreexports thatbootstrap-platform-admin.tsimports onorigin/mainand that the old core build lacked, not on this cut.packages/core/src/security/row-serving-door-exposure.pin.test.ts: 3 of 3 pass.node scripts/check-adr-symbol-anchors.mjsexit 0,pnpm check:query-options-erasureexit 0, and the symbol-anchor gates under Readers. None of these is derived for this change set; each was run by hand.dispatch-gates --commands: 64 commands. They are cut 4's 62, pluspnpm check:platform-checklistandpnpm --filter @objectstack/lint run check:doc-formula-expressions, which the checklist file adds. The--ranreconciliation shows 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 64 recorded an exit code and none is 3.Per command (exit code):
node scripts/check-ci-filter-parity.mjsexit 0node scripts/check-closing-keyword-parity.mjsexit 0node scripts/check-closing-keyword-parity.mjs --self-testexit 0node scripts/check-comment-mask-adoption.mjsexit 0node scripts/check-comment-mask-adoption.mjs --self-testexit 0node scripts/check-comment-mask-corpus.mjsexit 0node scripts/check-dts-emitted.mjs --self-testexit 0node scripts/check-issue-citations.mjsexit 0node scripts/check-keyed-text-bounds.mjsexit 0node scripts/check-keyed-text-bounds.mjs --self-testexit 0node scripts/check-platform-object-tenancy-census.mjsexit 0node scripts/check-platform-object-tenancy-census.mjs --self-testexit 0node scripts/check-plugin-teardown-shape.mjsexit 0node scripts/check-plugin-teardown-shape.mjs --self-testexit 0node scripts/check-position-name-fold-loaders.mjsexit 0node scripts/check-position-name-fold-loaders.mjs --self-testexit 0node scripts/check-registry-log-declared.mjsexit 0node scripts/check-registry-log-declared.mjs --self-testexit 0node scripts/check-rest-log-spy-declared.mjsexit 0node scripts/check-rest-log-spy-declared.mjs --self-testexit 0node scripts/check-scripts-symbol-anchors.mjsexit 0node scripts/check-scripts-symbol-anchors.mjs --self-testexit 0node scripts/check-system-context-census.mjsexit 0node scripts/check-system-context-census.mjs --self-testexit 0node scripts/check-tenant-audit-census.mjsexit 0node scripts/check-tenant-audit-census.mjs --self-testexit 0node scripts/check-undeclared-dep-imports.mjsexit 0node scripts/check-undeclared-dep-imports.mjs --self-testexit 0node scripts/docs-audit/check-affected-docs.mjsexit 0node scripts/docs-audit/check-drift-comment.mjsexit 0pnpm --filter @objectstack/lint run check:doc-formula-expressionsexit 0pnpm --filter @objectstack/spec run check:duration-unit-keysexit 0pnpm check:adr-anchorsexit 0pnpm check:agent-test-spellingexit 0pnpm check:bash32-floorexit 0pnpm check:cli-command-idsexit 0pnpm check:closed-ceremony-textexit 0pnpm check:cross-package-test-inputsexit 0pnpm check:doc-authoringexit 0pnpm check:driver-memory-censusexit 0pnpm check:dts-closureexit 0pnpm check:dual-build-cjs-loadsexit 0pnpm check:entry-guardexit 0pnpm check:error-status-conformanceexit 0pnpm check:gitlink-declaredexit 0pnpm check:i18nexit 0pnpm check:i18n-stale-fillexit 0pnpm check:issue-citationsexit 0pnpm check:lean-entry-closureexit 0pnpm check:logger-receiver-detachexit 0pnpm check:nul-bytesexit 0pnpm check:org-identifierexit 0pnpm check:page-declaration-shapeexit 0pnpm check:parse-guardexit 0pnpm check:platform-checklistexit 0pnpm check:pnpm-filter-targetsexit 0pnpm check:pnpm-flag-spellingsexit 0pnpm check:published-filesexit 0pnpm check:refd-timer-probeexit 0pnpm check:slot-lookupexit 0pnpm check:sourcemap-no-sources-contentexit 0pnpm check:test-source-aliasexit 0pnpm check:tier-file-adoptionexit 0pnpm check:watch-hint-literalexit 0pnpm check:query-options-erasureexit 0node scripts/check-adr-symbol-anchors.mjsexit 0node scripts/check-spec-docblock-symbol-anchors.mjsexit 0pnpm --filter @objectstack/spec check:livenessexit 0node scripts/check-platform-checklist.mjs --changed-from origin/mainexit 0Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa