Skip to content

Commit dd124e9

Browse files
refactor(plugin-security): move the read filters, field readers and object admissions out of security-plugin.ts (#25223 cut 9) (#25456)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuMYdF2uchcinMphEKW4sYxd)_ Part of #25223 Clause-②: no Metadata-protocol: none Ruling-ref: 6353073966 Cut 9 of the `security-plugin.ts` split: row 7 of the series plan, the read filters, the field readers and the object admissions. The claim is 6365993271. The row has three seams, so the bodies of nine methods move unedited out of the class to three new modules in this one PR, as cuts 6 and 8 split theirs over two. To `packages/plugins/plugin-security/src/security-plugin-read-filter.ts`: - `getReadFilter` (public), the row-level read scope the `security` service serves to the doors that compile their own statement: the same three layers the engine middleware ANDs into every find, the deny sentinel for a caller who resolves no set or carries no principal, fail-closed on any resolution failure. - `computeDelegatedReadFilter`, the same scope for an on-behalf-of caller: the principal's leg and the delegator's, intersected layer for layer (ADR-0090 D10, #25106). To `packages/plugins/plugin-security/src/security-plugin-field-readers.ts`: - `resolveProjectionFieldMask`, the derivation every published field projection shares: the field universe, the caller's sets (the fallback set on the metadata plane), the posture, the field map with the capability fold, and the delegator's intersection (#25107). - `computeReadableFields`, the readable projection both planes answer from (#8993). - `resolveCallerPosture`, the one posture a caller's gates read, with the stand-in for a caller who resolved no set (#20995, #21063). To `packages/plugins/plugin-security/src/security-plugin-object-admission.ts`: - `canReadObject` (public), the object-level read admission the read doors that bypass the middleware ask. - `objectReadAdmission`, that admission's arms over permission sets already resolved. - `canWriteObject` (public), the write admission the write preview asks (#18682). - `canExport` (public), the user-level export axis (#3544). 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 from `origin/main` at `25f88e899a`, cut 8's merge. It names no issue, so the claim comment is the identity record. **What stays in the class.** Each moved method keeps its name, signature, docblock and position, less `async`. Every caller still calls the methods: the service binding, the middleware and the write preview's seam in `start()`, and the four public projections. Four members of the row stay whole: `getReadableFields`, `getMetadataReadableFields`, `getWritableFields` and `getQueryableFields`. Each body is no longer than its stub would be. **Module-level declarations.** - `EMPTY_REQUIRED_PERMISSIONS`, the stand-in's empty capability contract, is read only by `resolveCallerPosture`'s body. It moves with it, unedited, to the field-readers module, as cuts 1 to 4 moved the declarations only their bodies read. - Its type, `NormalizedRequiredPermissions`, stays unexported in `security-plugin.ts`, where the posture interface and its normaliser read it. The module names it through the class's own signature: `ObjectSecurityMeta` as `getObjectSecurityMeta`'s resolved return type, as cut 1's module does, and the capability-contract type as that posture's `requiredPermissions` field. So the moved declaration's text is byte-identical. - The import name `intersectFieldMasks` (`./explain-engine.js`) is read only by `resolveProjectionFieldMask`'s body. It leaves `security-plugin.ts`'s import, and the field readers import it from the same specifier. No staying docblock links to it; `canWriteObject`'s docblock names it in backticks only. - The modules import two values that are already exported: `isPrincipalLessContext` (cut 6) and `requiredCapsForOperation`. `security-plugin.ts` gains no export in this cut, so the cut 6 `.d.ts` trap (an export whose signature names another file's type) does not arise. The `.d.ts` was measured anyway, in a local trial build before the claim: byte-identical to base (below). - Each value import is read only inside a function body, at call time, never while any of the four modules evaluates. `index.ts` still exports only `SecurityPlugin`, so the published surface does not move (measured below). **Two readers outside the moved code read the moved lines by path, and both are re-pointed here, as cut 6 re-pointed the same two.** - `content/docs/permissions/system-context.mdx`, rows 3, 4, 5, 6 and 6b. `check-system-context-census` keys each elevation read by file and enclosing symbol. The five `context?.isSystem` reads of this row (in `getReadFilter`, `resolveProjectionFieldMask`, `canExport`, `canReadObject` and `canWriteObject`) now sit in the new modules. A trial cut with the page untouched measured it: the census reports 8 problems, the three new files with no row and five anchors that are no longer read sites. Each of the five anchors keeps its symbol and changes only its path, from `security-plugin.ts#X` to the module's `#X`. No row's text changes, and no other anchor changes. With that, the census exits 0 (below). - `packages/plugins/plugin-security/src/delegated-organization-membership.test.ts`, the #25107 enumeration pin. It counts, per source file, the delegator resolutions and the organization-refusal asks, and lists the files that resolve one. Five moved bodies resolve a delegator, each beside its ask: `computeDelegatedReadFilter`, `resolveProjectionFieldMask`, `canReadObject`, `canWriteObject` and `canExport`. The expected list gains the three modules, and the title says six files instead of three. - The rule it pins, asks equal resolutions in every file, is unchanged. It holds at head: the read filter counts 1 and 1, the field readers 1 and 1, the object admission 3 and 3, and `security-plugin.ts` 2 and 2 (one in `start()`, one in `explainAccessForCaller`). **One ADR anchor entry is added**, in the claim amendment 6366124111: the field-readers module gets an entry carrying ADR-0106 and the ADR-0106 D7 paragraph of the `security-plugin.ts` entry, verbatim, because that paragraph's metadata-plane half is a branch of the moved derivation. See the ADR anchors section below. ## The published `.d.ts` does not change Under the series bar (ruling 6348891160 item 1), measured on base `25f88e899a` and on the head. The head build of `0feb79ca4f` and the full `turbo run build` at `b4ced7f87e` produced the same four `dist` files byte for byte: - Raw sha256 of `dist/index.d.ts` and `dist/index.d.mts`, base and head, all four the same: `7d67966ce82e00a1531807b3b0008f16c489fc2c3199f9ad67078302eb2803da`. The raw diff is 0 lines, so no union order moved either. - Normalised sha256, all four the same: `b53e9025f794e20debd2245afed5ceb1a0ad48109a2a21cc19232a562ccd9cb4`. - Base and head are both built against `25f88e899a`'s dependencies, rebuilt on that commit first. The hashes differ from those cuts 6 to 8 reported because `main` moved under the series: #24947 widened the spec's bulk-action `confirmText` to a template slot, and the `.d.ts` inlines that type in three places. The diff between cut 8's head `.d.ts` and this base is exactly those three hunks. - `security-plugin.ts` gains no export in this cut, and the modules import only values that were already exported. - The normaliser, run from `packages/plugins/plugin-security` (it sorts the members of each string-literal union and changes nothing else): ```sh node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum ``` - Positive controls on a copy of the head `dist/index.d.ts`, each a one-line edit: - Line 839 (`private computeDelegatedReadFilter;`) made public (`computeDelegatedReadFilter;`): raw `b46fd6c3c1ffaf2ede883a72a5d6fee1c692d38ab4d504639667880a0236ac09`, normalised `cadb75e96b0419b7bddb047925e7e3e8fab28abbed6e5ab161bd8f2a0d8a2bf6`. - Line 839 renamed (`private computeDelegatedReadFilterRenamed;`): raw `1a5427bc1e748676c425f150b4e402dc29823002fe9e10389c7720c1106367dc`, normalised `365336182c2d65b2021c3844251cc211787143f0fdbcb60af1418ed1e8daf73a`. - Line 1246, the public `canExport` signature, with `context?: any` changed to `context?: unknown`: raw `f0088fc10e9713f29fc84a6ec7fde84f3b5005556ca001ceb7bae565f20d6ab0`, normalised `f8bfba0031d62ff891175079526ff10f4017ffd0cf7148c61e220d1092777115`. - All three differ from the real hashes, so the comparison does see a visibility change, a rename and a public signature change. - The four public methods carry their full signatures and docblocks in the `.d.ts`: `getReadFilter` (line 801), `canReadObject` (1045), `canWriteObject` (1227) and `canExport` (1246). The five private ones carry their name and docblock only: `computeDelegatedReadFilter` (839), `computeReadableFields` (896), `resolveProjectionFieldMask` (950), `resolveCallerPosture` (995) and `objectReadAdmission` (1080). Those lines are the same in both files. - Export tables: the ESM and CJS bundles each export the same 66 names at base and head. The lists were loaded at runtime (`require` and `import()` of the built bundles), not parsed. The sorted name list hashes `83576430f83cf60cecbcc5c0e80214f16c626a8e66f700294fc6a44361423116` on all four, as at cuts 6 to 8. - The bundle JS bytes differ (`index.js` `3a109ef0` to `dfa83a54`, `index.mjs` `1c38ee91` to `18c4d919`), because code moved within them. Every value the new modules import comes from a file inside this package, so the set of external specifiers each bundle loads is the same at base and head (16 distinct `require` specifiers in `index.js`, 17 distinct `from` specifiers in `index.mjs`). No changeset: `skip-changeset`, per ruling 6348891160 item 4. ## What moved The base line numbers are each method's signature and closing brace at `25f88e899a`. The last column lists the members the body reads, which is exactly what its host carries. | method | base lines | module | host members | |---|---|---|---| | `getReadFilter` (public, async) | `:5871`-`:5954` | read-filter | `computeControlledByParentFilter`, `computeDelegatedReadFilter`, `computeRlsFilter`, `logger`, `resolvePermissionSetsForContext`, `resolveSharingReadFilter` | | `computeDelegatedReadFilter` (private, async) | `:5993`-`:6055` | read-filter | `computeControlledByParentFilter`, `computeRlsFilter`, `getObjectSecurityMeta`, `logger`, `permissionEvaluator`, `ql`, `resolvePermissionSetsForContext`, `resolveSharingReadFilter`, `tenancyPosture` | | `computeReadableFields` (private, async) | `:6119`-`:6140` | field-readers | `resolveProjectionFieldMask` | | `resolveProjectionFieldMask` (private, async) | `:6207`-`:6300` | field-readers | `computeQueryGuardFieldPerms`, `computeReadPartialMaskRules`, `foldFieldRequiredPermissions`, `getObjectFieldNames`, `metadata`, `permissionEvaluator`, `ql`, `resolveCallerPosture`, `resolveFallbackPermissionSets`, `resolvePermissionSetsForContext`, `tenancyPosture` | | `resolveCallerPosture` (private, async) | `:6346`-`:6365` | field-readers | `getObjectSecurityMeta` | | `canReadObject` (public, async) | `:6416`-`:6457` | object-admission | `logger`, `objectReadAdmission`, `ql`, `resolvePermissionSetsForContext`, `tenancyPosture` | | `objectReadAdmission` (private, async) | `:6493`-`:6532` | object-admission | `getObjectSecurityMeta`, `permissionEvaluator` | | `canWriteObject` (public, async) | `:6680`-`:6846` | object-admission | `computeForbiddenFieldWrites`, `delegatedAdminGate`, `getObjectSecurityMeta`, `logger`, `organizationWallRefusal`, `permissionEvaluator`, `ql`, `resolvePermissionSetsForContext`, `tenancyPosture` | | `canExport` (public, async) | `:6866`-`:6913` | object-admission | `getObjectSecurityMeta`, `permissionEvaluator`, `ql`, `resolvePermissionSetsForContext`, `tenancyPosture` | Each stub drops `async` and returns the moved function's promise, as cuts 1 and 3 to 8 did for their async methods. Four of the nine methods are public, so the `.d.ts` carries their full signatures. A body move does not touch a signature, and those lines do not change (below). ## The shape (#23050 shape B, as cuts 1 to 8 applied it) - The class keeps each method with its name, signature, docblock and position, because tsup emits members with their JSDoc into the `.d.ts`. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25223 cut 9]`. - `ReadFilterHost`, `FieldReadersHost` and `ObjectAdmissionHost` are new. Each types every member as the class member itself (`SecurityPlugin['m']`). Each moved function types its `this` as the `Pick` of its host over exactly the members its body reads, and the stub annotates its literal with the same `Pick`. There is no cast, and no member becomes public or protected. - Methods go in as late-bound closures that call `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 read scope still asks the plugin's RLS filter, controlled-by-parent leaf, sharing filter and set resolution; - the projection derivation still asks the plugin's field universe, fallback sets, posture and field folds, and the two closures it returns (`readPartialMaskRules`, `readQueryGuardPerms`) still call the plugin when they are called; - the admissions still ask the plugin's arms, organization wall and forbidden-field check. No test replaces a moved method on the plugin instance today; one that did would still replace it for every caller. No moved body calls a function-valued field, so no receiver changes. `delegatedAdminGate`, `logger`, `metadata`, `permissionEvaluator` and `ql` are objects whose own methods the bodies call. - Fields follow the rule cut 1 set, on each field's own declaration. None of the six fields the bodies read is declared `readonly` (`delegatedAdminGate`, `logger`, `metadata`, `permissionEvaluator`, `ql`, `tenancyPosture`), so all go as getters, and each read happens when the body makes it. `start()` assigns all of them but `permissionEvaluator` after construction, and tests replace `permissionEvaluator` on a constructed plugin. - The hosts declare every field `readonly`, so a moved body cannot assign a member through them. The typecheck passes, so none does. - Nothing re-exports any of the modules, so the package entry does not move. ## Proof of move-only - **Bodies, by AST token**: each moved function's body has the same TypeScript token sequence as the base method body, and each of the 526 body lines is the base line with two leading spaces removed. Parameters, return types and `async` match. `resolveProjectionFieldMask`'s multi-line return type, with the docblock of its member inside it, is repeated on the function one indentation level in, as cut 7 did; its 82 tokens are equal. Three negative controls fail the check: - in `getReadFilter`, `if (context?.isSystem) return undefined;` changed to `if (!context?.isSystem) …`; - in `resolveProjectionFieldMask`, the system answer `fields: allFields` changed to `fields: []`; - in `canExport`, `checkObjectPermission('export', …)` changed to `checkObjectPermission('read', …)`. - **Class side**: each moved method keeps its docblock and signature, less `async`. Every other class member, 117 named members plus the constructor, is byte-identical to base. That includes the four staying members above. The class has 127 members at base and at head. - **Top level of `security-plugin.ts`**: 102 of the 104 base top-level statements other than the class are byte-identical to base. The other two: - the `./explain-engine.js` import loses `intersectFieldMasks`; - `EMPTY_REQUIRED_PERMISSIONS` is gone from the file. The field-readers module holds the same statement, byte for byte. The three new statements are the imports of the new modules. - **The two re-pointed readers**: the page diff is five lines, each one anchor's path. The pin diff is the title's count and the expected list. - **git's own view** (`git diff --color-moved=plain --color-moved-ws=allow-indentation-change 25f88e8 HEAD` over the four source files): of the 538 deleted lines, git sees 528 as moved. The other 10 are the nine signatures that lose `async` and the import line `intersectFieldMasks,`. Of the 1,044 added lines, 561 show as moved, all in the three new modules: 524 of the 526 body lines, the 3 lines of `EMPTY_REQUIRED_PERMISSIONS`, and 34 lines whose text git also finds among the deleted lines (20 blank lines, 12 closing braces, 2 lines of the new function signatures). The 2 body lines git leaves uncoloured are the closing `};` of `resolveProjectionFieldMask`'s and `resolveCallerPosture`'s return literals, which git pairs with a stub's `};` of the same text rather than with a deletion. The other 483 added lines are the modules' headers, imports, host interfaces, type aliases and function signatures, the nine stubs, and the three new imports with their comments. - **Size**: 1,610 changed lines (+1,065 / -545) over 7 files, under the 3,000-line manual-merge line. `security-plugin.ts` goes from 9,248 to 8,852 lines; `security-plugin-read-filter.ts` has 241, `security-plugin-field-readers.ts` 252 and `security-plugin-object-admission.ts` 409. The other three are the page (+5 / -5), the pin (+9 / -2) and the new anchor entry (+7). ## ADR anchors (ruling 6353073966 item 2) - `node scripts/check-adr-symbol-anchors.mjs`: exit 0 at base `25f88e899a` and exit 0 at head, both with `2384 anchors across 142 records resolve — 466 symbol (440 declaration, 26 literal), 1890 file-level, 28 cross-repo, 6 exempt, 18 continuation`. CI runs this gate (`lint.yml`), but `dispatch-gates --commands` does not derive it for this change set (#21697), so it is run by hand here. - `pnpm check:adr-anchors`: exit 0 at base and at head. It counts 80 anchored files at base and 81 at head, the new entry below. - **One new entry, in the claim amendment 6366124111.** It is `scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-field-readers.ts.json`, and it carries ADR-0106 and the ADR-0106 D7 invariant paragraph of the `security-plugin.ts` entry, verbatim, as the entries of cuts 2 to 5 carry theirs. - That paragraph names `getMetadataReadableFields` and `getReadableFields`, which stay. Its metadata-plane half, "the same caller resolves the configured fallback permission set", is the `fallbackOnEmptySets` branch of `resolveProjectionFieldMask`, whose body moved here with its `[ADR-0106 D7]` comment. Cut 4's entry followed the same rule: its module got one because the "reads answer RLS_DENY_FILTER" half of the ADR-0055 §1 paragraph was the leaf it moved. - The `security-plugin.ts` entry is unchanged, because the two methods, their docblocks and the flag each passes stay in that file. - No other paragraph describes a moved body. ADR-0055 §1 is the controlled-by-parent relation (cuts 2 to 4). ADR-0123 D2 is the write wall's no-organization refusal (cut 5); `canWriteObject` only asks that wall's scope pre-test, as the middleware does. ADR-0138 D1 is the seed-ownership claim. So the read-filter and object-admission modules get no entry, as the modules of cuts 1 and 6 to 8 have none. - **The decision-letter count on the anchored surface goes from 2001 to 1978.** That gate counts `ADR-NNNN Dk` citations in each anchored file and in each entry's own invariant text. - 24 citations leave the anchored surface with the read-filter and object-admission bodies: - `getReadFilter` 4: ADR-0056 D2, ADR-0090 D10, ADR-0096 D5, ADR-0139 D5; - `computeDelegatedReadFilter` 4: ADR-0090 D10 three times, ADR-0123 D2; - `canReadObject` 3 and `canExport` 3: each ADR-0056 D2, ADR-0090 D10, ADR-0096 D5; - `objectReadAdmission` 2: ADR-0066 D3, ADR-0090 D10; - `canWriteObject` 8: ADR-0056 D2, ADR-0066 D3, ADR-0090 D10 twice, ADR-0090 D12, ADR-0096 D5, ADR-0123 D2 twice. - `resolveProjectionFieldMask`'s 3 (ADR-0090 D10, ADR-0096 D5, ADR-0106 D7) leave `security-plugin.ts` but stay on the anchored surface, in the field-readers module. - One arrives: the new entry's invariant is the ADR-0106 D7 paragraph, and it names its own decision letter once. The three new import comments and the module headers cite none. - The amendment 6366124111 gave 1977. It left out that one citation in the entry's own text, which the gate counts. The measured head count is 1978. - Every moved citation is still in the tree, in the module its body moved to. The tree-wide citations that resolve go from 52175 across 6636 files at base to 52179 across 6640 at head. The three modules add files and no citations, and the new entry adds one file and four citations. - `docs/adr/**` is not touched, and no anchor token is planted. - The anchor map covers every ADR symbol anchor naming `security-plugin.ts`: 27 anchors, 18 symbols. It was measured with `extractAnchors` and `symbolSegmentResolution` from `scripts/symbol-anchors.mjs`, which is measurement, not a new gate. It was read on `25f88e899a` BEFORE the claim, with the whole cut applied as a local trial: no anchor changed class. Every anchor resolves at head in `security-plugin.ts` with its base class. The table gives each anchor's class at base and head and the line its own declaration resolves on. Every anchor stays in `security-plugin.ts`. | anchor | cited at | base class · line (member) | head class · line (member) | |---|---|---|---| | `#CBP_MAX_CHAIN_DEPTH` | ADR-0055:135 | declaration · `:688` (top-level) | declaration · `:712` (top-level) | | `#SecurityPlugin` | ADR-0049:49, ADR-0138:248, ADR-0138:265 | declaration · `:1320` (class) | declaration · `:1344` (class) | | `#SecurityPlugin.getMetadataReadableFields` | ADR-0138:153 | declaration · `:6115` (method) | declaration · `:6030` (method) | | `#SecurityPlugin.resolvePermissionSetsForContextUnmemoized` | ADR-0138:427 | declaration · `:7020` (method) | declaration · `:6624` (method) | | `#assertControlledByParentWrite` | ADR-0055:58, ADR-0055:125 | declaration · `:8505` (method) | declaration · `:8109` (method) | | `#assertMasterRowEditable` | ADR-0055:125 | declaration · `:8631` (method) | declaration · `:8235` (method) | | `#collectRLSPolicies` | ADR-0105:93 | declaration · `:8685` (method) | declaration · `:8289` (method) | | `#composeWriteTenantWall` | ADR-0123:4 | declaration · `:7843` (method) | declaration · `:7447` (method) | | `#computeControlledByParentFilter` | ADR-0055:5, ADR-0055:123, ADR-0139:246 | declaration · `:8151` (method) | declaration · `:7755` (method) | | `#computeDelegatedReadFilter` | ADR-0139:223 | declaration · `:5993` (method) | declaration · `:5949` (method) | | `#computeRlsFilter` | ADR-0055:24 | declaration · `:7551` (method) | declaration · `:7155` (method) | | `#getReadFilter` | ADR-0096:181, ADR-0096:182, ADR-0096:183, ADR-0096:199, ADR-0139:222 | declaration · `:5871` (method) | declaration · `:5895` (method) | | `#hasPlatformAdminPosture` | ADR-0099:317 | declaration · `:7578` (method) | declaration · `:7182` (method) | | `#isPrincipalLessContext` | ADR-0138:152 | declaration · `:561` (top-level) | declaration · `:585` (top-level) | | `#manage_metadata` | ADR-0099:68 | literal · `:310` (top-level) | literal · `:338` (top-level) | | `#nav_permission_sets` | ADR-0126:406 | literal · `:1748` (`init`) | literal · `:1772` (`init`) | | `#rlsFilter` | ADR-0055:24 | declaration · `:4322` (`start`) | declaration · `:4346` (`start`) | | `#writeCheckPolicies` | ADR-0066:112 | declaration · `:1215` (top-level) | declaration · `:1239` (top-level) | ## Readers Two readers outside the moved code read the moved lines by path, the system-context page and the #25107 enumeration pin, and both are re-pointed in this PR (above). No other gate, baseline, docs anchor or test reads the moved lines by path. Each kind was read: - **The system-context census.** `node scripts/isystem-census.mjs --json` gives the same 148 sites at base and head, compared without line numbers and with the three modules mapped back to `security-plugin.ts`: 0 differences. Five of the head sites are in the modules, the five `context?.isSystem` reads the page's re-pointed anchors name: `getReadFilter` (read-filter), `resolveProjectionFieldMask` (field-readers), and `canReadObject`, `canWriteObject` and `canExport` (object-admission). The role counts are the same (`read` 153, `declaration` 38, `key` 438, `other` 9), and so are the 5 non-elevation reads and the 22 packages. The files holding a site go from 71 to 74 and the scanned files from 393 to 396, the three modules. `check-system-context-census` exits 0 at base and head with the same totals (148 sites, 127 symbols, 140 cited against 140 required, 163 anchors) and files 71 to 74, and its `--self-test` exits 0 at both. With the page left untouched it would report 8 problems (claim). - **The tenant-audit census.** No moved body holds an engine write: the bodies hand `ql` to the delegator resolution and the field universe, and `canWriteObject` asks it for `getSchema`. `node scripts/tenant-audit-census.mjs --json` gives the same 238 write call sites at base and head, compared the same way: 0 differences, none in the modules, the same totals, the same 174 non-engine subtractions, 0 unresolved and 0 unledgered. Only `scannedSources` moves, 724 to 727: the three modules. `check-tenant-audit-census` and its `--self-test` exit 0 at base and head with the same output, and `tenant-audit-census.mdx` is not touched. - **The #25107 enumeration pin** is described above. It is part of the plugin-security suite, which passes at head with the same counts as base. - **`packages/core/src/security/row-serving-door-exposure.pin.test.ts`** classifies every non-test caller of the read admission, `.canReadObject(`. No moved body calls it: the `canReadObject` body is the admission's definition, not a call. `security-plugin.ts` still calls it, from `describePositionGrants` and from the service binding in `start()`, so its row stays true and its "still calls it" check still fires. **`operation-private-keys.pin.test.ts`** in the same directory finds every declaration of `withoutOperationPrivateKeys` and the key prefix; no moved body names either. Run on their own at head: 8 of 8 pass. Open PRs #25389 and #25410 change the first pin's scan scope (an extension-scoped prefilter), not what it discriminates on, and touch none of this cut's files. - **The spec liveness ledger.** `packages/spec/liveness/permission.json` cites `security-plugin.ts#canExport` as evidence for `allowExport`. The anchor resolves on the method, which stays with its docblock, and `allowExport` is still named in `security-plugin.ts` (in that docblock), so the key-mention reading does not change. `pnpm --filter @objectstack/spec check:liveness` gives byte-identical output at base and head. The entry's prose, `canExport` asking `checkObjectPermission('export')`, stays true by method: the body is one call away. `packages/spec/**` is not touched. - **Other readers that name the file in code.** Besides the two pins above, the code that names `security-plugin.ts` by path is: the `check-system-context-census.mjs` row for the file (keyed on `start`, which this cut does not touch), the spec liveness ledger's file-level evidence in `field.json`, the file's `scripts/adr-anchors/` entry, and the citation string in `measure-position-name-fold-census.mjs`. None reads text inside the moved bodies. The tree-wide scanners that read every source file are the gates and censuses below, each run at base and head. - **The platform checklist.** `check-platform-checklist` exits 0 at base and head with the same output. No checklist citation names a moved symbol or a local inside a moved body. - **Symbol anchors outside the ADRs.** Every path-qualified `plugin-security/src/security-plugin*.ts#…` anchor outside `docs/adr/` was resolved in its own file at base and at head: 32 at both, 0 unresolved. At head, 23 point into `security-plugin.ts`, the five re-pointed ones into the new modules (1, 1 and 3), and 4 into the cut 5 and cut 3 modules, as at base. The non-ADR corpora that cuts 2 to 8 read (spec liveness, platform checklist, `system-context.mdx`, spec `conversions/registry.ts`) hold 27 anchors over 17 symbols naming `security-plugin.ts`; with `symbolSegmentResolution`, each resolves at head with its base class: 0 changed. - **Readers by name, not by path.** These were read and none needs a change, because each method still exists in the class under the same name and signature: - the behaviour tests of the read scope, the projections, the admissions and the export axis in this package. Each drives the plugin; none calls or replaces a moved method directly; - stand-ins in other packages that offer their own `getReadFilter` or `canReadObject` (`engine-readable-ids-composition.test.ts`, `parent-record-read-admission.integration.test.ts` and the REST and storage tests). They replace a method on their own fake service object, not on the plugin; - ADR-0139's prose naming `computeDelegatedReadFilter`, and the system-context page's row text naming `getReadableFields`, `getWritableFields`, `canExport`, `canReadObject` and `canWriteObject`, which stay true by method. - **Line citations**: no `security-plugin.ts:N` citation in the repo points into the moved ranges (base `:5871`-`:6913`). The 13 citations found each already pointed at an unrelated line before cut 2, so this cut does not make any of them wrong. - **Docs Drift Check**, re-derived with `node scripts/docs-audit/affected-docs.mjs --json 25f88e8` (advisory): 41 pages. The derivation reads every name in the three added files as touched, so it reaches pages through names the move only relocated (a page can be reached by more than one): - `requiredPermissions` (29 pages), the indexed-access key in the field readers' `NormalizedRequiredPermissions` alias; - `permissionSets` (10), a local of the moved bodies; - `SecurityPlugin` (6), each module's `import type`; - the moved methods' own names: `getReadFilter` (4), `canReadObject` (3), `canExport` (2), `canWriteObject` (2) and `resolveProjectionFieldMask` (1). Each of those members keeps its name, signature, docblock and behaviour, so no page goes stale. The one page among them that reads the moved lines by path, `system-context.mdx`, is re-pointed in this PR (above), and every path-qualified anchor and line citation into the file was checked separately (above). - **Census gates, run on base `25f88e899a` and head and compared:** - All 104 commands (the 98 derived and the 6 hand-run below) exit 0 at head. 63 give identical output at base and head, once the worktree path and timings are masked. That includes `check-adr-symbol-anchors`, `check-platform-checklist`, `check-scripts-symbol-anchors`, `check-platform-object-tenancy-census`, `check-tenant-audit-census`, `check-registry-log-declared`, `check-rest-log-spy-declared`, `check:cross-package-test-inputs`, `check:driver-memory-census`, `check:query-options-erasure` and the spec liveness ledger. - The other 41, each read line by line: - `check:adr-anchors`, `check-system-context-census` and the two census `--json` reads move as described above. - Scanned counts that move by the three modules: `check-plugin-teardown-shape` 9384 to 9387 sources, `check-undeclared-dep-imports` 3310 to 3313 files and 2859 to 2862 specifiers (the modules import only relative files and `@objectstack/spec`, a declared dependency), `check:doc-formula-expressions` 477 to 480 files, the spec package's `check:duration-unit-keys` 3315 to 3318, `check:closed-ceremony-text` 4553 to 4556, `check:engine-double-contract` 3619 to 3622 production files, `check:error-status-conformance` 3407 to 3410, `check:org-identifier` 3652 to 3655, `check:refd-timer-probe` 9983 to 9986, `check:type-check-coverage` and `check:type-check-debt` 3536 to 3539 walked sources, and `check:skill-identifier-liveness` 139729 to 139733 implementation word tokens. - Counts that move by four, the modules and the docs build's `apps/docs/next-env.d.ts` (gitignored, in the head worktree only): `check-comment-mask-corpus` 9989 to 9993, `check:logger-receiver-detach` 3626 to 3630, `check:page-declaration-shape` 3659 to 3663. - Tracked-file counts that move by four, the modules and the new anchor entry: `check-closing-keyword-parity` and `check:gitlink-declared` 11997 to 12001, `check:nul-bytes` 11988 to 11992. `check:pnpm-flag-spellings` reads 449 files under `scripts/` instead of 448 and leaves 123 data files unread instead of 122: the entry is JSON. `check:agent-test-spelling` reads 608 files instead of 607 (the entry, under `scripts/`) and 87 manifests instead of 86 (a `package.json` the docs build wrote under `apps/docs/.next`). - Docs counts that move because the head worktree holds three gitignored generated pages, `content/docs/protocol-upgrade/{17,18,index}.md`, that the base worktree does not, as at cuts 6 to 8 (CI's checkout does not have them): `check-docs-section-name` 418 to 421 files, `check:corpus-claim-drift` 259 to 262, `check:doc-anchors` and `check:doc-authoring` 424 to 427, `check:react-page-adapter-contract` 399 to 402, `check-doc-route-spelling --advisory` 745 to 885 literals (0 flagged at both), and `check:vendor-version-stamps` 10378 to 10385 files with 7 more `better-auth` stamps, all 7 in the generated `18.md`. `check:doc-authoring` also parses 1432 sources instead of 1429, the modules. - `check-position-name-fold-loaders` walks the working tree: 12350 to 12357 files, with the same 8 references (6 test, 2 declared instrument, 0 loader). - `check:dts-closure` and `check:sourcemap-no-sources-content` sweep built packages, and the head worktree has more of them built (76 and 72) than the base worktree (19). Both pass at both. - `check-issue-citations` judges a change set, so it has nothing to read at base. At head it judged 56 citations across 4 files, and all resolve. - `check:merge-driver` differs only in the names of its temporary directories. - Five answer non-zero at base only, each a missing or stale build in the base worktree rather than a verdict: the spec package's `check:docs` (exit 1, its generated schema is absent until `gen:schema`), `check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n` (exit 3, prerequisite not met), and `check:docs-transcript-drift` (exit 1). The last reads the built `@objectstack/lint` registry, and the base worktree's build predated #24947's added rule, so it read 50 rules against the pages' 51. That package's `dist` in the base worktree was rebuilt later in the same run, and a rerun at base then exits 0 with the head's output. - Six more gates keep a roster in a directory this change touches (`scripts/` or `packages/`), so `dispatch-gates` marks their silence as no evidence. Each was run at base and head: `check-published-list-mirrors`, `check:authz-resolver`, `check:console-injection`, `check:error-code-casing`, `check:filter-alias-parity` and `check:published-readme-exports`. All exit 0 at both with identical output, except that `check:error-code-casing` scans 9370 files instead of 9367, the modules. ## Acceptance notes - Nothing outside this cut is touched: no other section of the class, no behaviour, no `docs/adr/**`, no existing `scripts/adr-anchors/` entry, no census ledger, and no `packages/spec/**`. Outside the package, only two files change: the five anchor paths on the system-context page, and the one new anchor entry. - Serial check, also in the claim: of the 24 open PRs at claim time, none touched `security-plugin.ts`, the new module names, `system-context.mdx`, `delegated-organization-membership.test.ts`, the census scripts, the liveness ledger or the plugin-security anchor entry. - Since the local runs, `origin/main` gained 13 commits, to `2581efbcbd`. None touches this cut's files. The closest, `tenant-audit-census.mdx` and its counts file from #25423 (cut 8 of #25224), are files this cut reads through its gates and does not edit. On a scratch merge of this head onto `2581efbcbd`, `check-adr-symbol-anchors`, `check:adr-anchors`, `check-platform-checklist`, `check-system-context-census`, `check-tenant-audit-census` and the `isystem-census` read all exit 0. There, `check:adr-anchors` counts 81 anchored files and 1978 decision-letter citations, the same as this head, and the system-context census counts 148 sites across 74 files. - What is left of the plan's row 7 in `security-plugin.ts` after this cut: the four projections that stay whole and the nine method shells. `security-plugin.ts` has 8,852 lines at head, 9,248 at base. Next is row 8 (explain, sharing verdicts, authored row write, baselines), then `start()`, which is Tier H and goes to the seat before any PR. ## Gates All gates ran against base `25f88e899a`. The package runs (its test, typecheck, eslint and build, and the two core pins) ran on the tree of `0feb79ca4f`, the first commit. The second commit, `b4ced7f87e`, adds only the anchor entry JSON under `scripts/adr-anchors/`, which none of those runs reads. Every other gate ran on `b4ced7f87e`. - `pnpm --filter @objectstack/plugin-security test` (`vitest run --maxWorkers=3`, under the verify lock): Test Files 214 passed (214), Tests 4465 passed, 45 skipped (4510). Base `25f88e899a` gives the same counts. VERDICT command-exit 0 on both. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0. It runs `tsc --noEmit`, the scripts tsconfig, and `check:test-typecheck`. - `eslint --no-inline-config` over the five touched source and test files: exit 0. - `pnpm --filter @objectstack/plugin-security build`: exit 0. It produced the `.d.ts` and bundles measured above. The package's dependencies were rebuilt on `25f88e899a` first. - `row-serving-door-exposure.pin.test.ts` and `operation-private-keys.pin.test.ts` (`@objectstack/core`): 8 of 8 pass. - Six commands are not derived for this change set, and the reconciliation names them: `node scripts/check-adr-symbol-anchors.mjs`, `check-platform-checklist` (the script and `pnpm check:platform-checklist`), `pnpm --filter @objectstack/spec check:liveness` and the two census `--json` reads. Each was run by hand. `pnpm check:adr-anchors`, `check-scripts-symbol-anchors` (and `--self-test`), `pnpm check:query-options-erasure` and the spec liveness check (spelled `run check:liveness`) are derived for it this time. - `dispatch-gates --commands`: 98 commands. The `--ran` reconciliation shows 98 derived, 98 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 98 recorded an exit code and none is 3. The six hand-run commands outside the derivation are recorded beside them. Per command (exit code), the derived first and the hand-run after: - `node scripts/check-ci-filter-parity.mjs` exit 0 - `node scripts/check-closing-keyword-parity.mjs` exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` exit 0 - `node scripts/check-comment-mask-adoption.mjs` exit 0 - `node scripts/check-comment-mask-adoption.mjs --self-test` exit 0 - `node scripts/check-comment-mask-corpus.mjs` exit 0 - `node scripts/check-doc-frontmatter.mjs` exit 0 - `node scripts/check-doc-frontmatter.mjs --self-test` exit 0 - `node scripts/check-doc-route-spelling.mjs --advisory` exit 0 - `node scripts/check-doc-route-spelling.mjs --self-test` exit 0 - `node scripts/check-docs-section-name.mjs` exit 0 - `node scripts/check-docs-section-name.mjs --self-test` exit 0 - `node scripts/check-dts-emitted.mjs --self-test` exit 0 - `node scripts/check-issue-citations.mjs` exit 0 - `node scripts/check-keyed-text-bounds.mjs` exit 0 - `node scripts/check-keyed-text-bounds.mjs --self-test` exit 0 - `node scripts/check-platform-object-tenancy-census.mjs` exit 0 - `node scripts/check-platform-object-tenancy-census.mjs --self-test` exit 0 - `node scripts/check-plugin-teardown-shape.mjs` exit 0 - `node scripts/check-plugin-teardown-shape.mjs --self-test` exit 0 - `node scripts/check-position-name-fold-loaders.mjs` exit 0 - `node scripts/check-position-name-fold-loaders.mjs --self-test` exit 0 - `node scripts/check-registry-log-declared.mjs` exit 0 - `node scripts/check-registry-log-declared.mjs --self-test` exit 0 - `node scripts/check-rest-log-spy-declared.mjs` exit 0 - `node scripts/check-rest-log-spy-declared.mjs --self-test` exit 0 - `node scripts/check-scripts-symbol-anchors.mjs` exit 0 - `node scripts/check-scripts-symbol-anchors.mjs --self-test` exit 0 - `node scripts/check-section-landing-index.mjs` exit 0 - `node scripts/check-section-landing-index.mjs --self-test` exit 0 - `node scripts/check-system-context-census.mjs` exit 0 - `node scripts/check-system-context-census.mjs --self-test` exit 0 - `node scripts/check-tenant-audit-census.mjs` exit 0 - `node scripts/check-tenant-audit-census.mjs --self-test` exit 0 - `node scripts/check-undeclared-dep-imports.mjs` exit 0 - `node scripts/check-undeclared-dep-imports.mjs --self-test` exit 0 - `node scripts/docs-audit/check-affected-docs.mjs` exit 0 - `node scripts/docs-audit/check-drift-comment.mjs` exit 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` exit 0 - `pnpm --filter @objectstack/lint run check:doc-security-posture` exit 0 - `pnpm --filter @objectstack/spec run check:docs` exit 0 - `pnpm --filter @objectstack/spec run check:duration-unit-keys` exit 0 - `pnpm --filter @objectstack/spec run check:empty-state` exit 0 - `pnpm --filter @objectstack/spec run check:liveness` exit 0 - `pnpm --filter @objectstack/spec run check:skill-examples` exit 0 - `pnpm --filter @objectstack/spec run check:strictness-ledger` exit 0 - `pnpm --filter @objectstack/spec run check:variant-docs` exit 0 - `pnpm --filter @objectstack/spec run check:yaml-examples` exit 0 - `pnpm check:adr-anchors` exit 0 - `pnpm check:agent-test-spelling` exit 0 - `pnpm check:bash32-floor` exit 0 - `pnpm check:cli-command-ids` exit 0 - `pnpm check:closed-ceremony-text` exit 0 - `pnpm check:corpus-claim-drift` exit 0 - `pnpm check:cross-package-test-inputs` exit 0 - `pnpm check:doc-anchors` exit 0 - `pnpm check:doc-authoring` exit 0 - `pnpm check:docs-audit-scope` exit 0 - `pnpm check:docs-redirects` exit 0 - `pnpm check:docs-single-h1` exit 0 - `pnpm check:docs-spec-enumerations` exit 0 - `pnpm check:docs-transcript-drift` exit 0 - `pnpm check:driver-memory-census` exit 0 - `pnpm check:dts-closure` exit 0 - `pnpm check:dual-build-cjs-loads` exit 0 - `pnpm check:engine-double-contract` exit 0 - `pnpm check:entry-guard` exit 0 - `pnpm check:error-status-conformance` exit 0 - `pnpm check:gitlink-declared` exit 0 - `pnpm check:i18n` exit 0 - `pnpm check:i18n-stale-fill` exit 0 - `pnpm check:issue-citations` exit 0 - `pnpm check:lean-entry-closure` exit 0 - `pnpm check:logger-receiver-detach` exit 0 - `pnpm check:merge-driver` exit 0 - `pnpm check:nul-bytes` exit 0 - `pnpm check:objectql-double-limit` exit 0 - `pnpm check:org-identifier` exit 0 - `pnpm check:page-declaration-shape` exit 0 - `pnpm check:parse-guard` exit 0 - `pnpm check:pnpm-filter-targets` exit 0 - `pnpm check:pnpm-flag-spellings` exit 0 - `pnpm check:published-files` exit 0 - `pnpm check:published-readme-links` exit 0 - `pnpm check:query-options-erasure` exit 0 - `pnpm check:react-page-adapter-contract` exit 0 - `pnpm check:refd-timer-probe` exit 0 - `pnpm check:role-word` exit 0 - `pnpm check:skill-identifier-liveness` exit 0 - `pnpm check:slot-lookup` exit 0 - `pnpm check:sourcemap-no-sources-content` exit 0 - `pnpm check:test-source-alias` exit 0 - `pnpm check:tier-file-adoption` exit 0 - `pnpm check:type-check-coverage` exit 0 - `pnpm check:type-check-debt` exit 0 - `pnpm check:vendor-version-stamps` exit 0 - `pnpm check:watch-hint-literal` exit 0 - `pnpm check:where-matcher` exit 0 - `node scripts/check-adr-symbol-anchors.mjs` exit 0 - `node scripts/check-platform-checklist.mjs` exit 0 - `pnpm check:platform-checklist` exit 0 - `node scripts/isystem-census.mjs --json` exit 0 - `node scripts/tenant-audit-census.mjs --json` exit 0 - `pnpm --filter @objectstack/spec check:liveness` exit 0 The six roster gates run by hand (above): - `node scripts/check-published-list-mirrors.mjs` exit 0 - `pnpm check:authz-resolver` exit 0 - `pnpm check:console-injection` exit 0 - `pnpm check:error-code-casing` exit 0 - `pnpm check:filter-alias-parity` exit 0 - `pnpm check:published-readme-exports` exit 0 --- _Generated by [Claude Code](https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea33919 commit dd124e9

7 files changed

Lines changed: 1065 additions & 545 deletions

‎content/docs/permissions/system-context.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ that silently does not happen.
9898
|:--|:---|:---|:---|:---|
9999
| 1 | **The whole security middleware short-circuits** before any gate runs | plugin-security | Get: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: every gate under it in this lane at once, down to and including the write-bypass row that widens the effective write scope to `org` (row 7) — this is the single largest behaviour on the page | `packages/plugins/plugin-security/src/security-plugin.ts#start` |
100100
| 2 | **`owner_id` is not auto-stamped on INSERT** (the step 3.5 anchor guard is inside the block row 1 skips) | plugin-security | Lose: the row lands `owner_id = NULL`, so the default `owner_only_writes` policy hides it **from its own creator**. Get: nothing — this is a gap, not a capability | the step 3.5 guard block and the short-circuit that skips it are both inside `packages/plugins/plugin-security/src/security-plugin.ts#start` |
101-
| 3 | Row-level read filter resolves to "no filter" | plugin-security | Get: unscoped reads. Lose: row-level scoping entirely | `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` |
102-
| 4 | Field-level security returns **all** fields | plugin-security | Get: every column readable (`getReadableFields`) and writable (`getWritableFields`). Lose: field masking | `packages/plugins/plugin-security/src/security-plugin.ts#resolveProjectionFieldMask` |
103-
| 5 | Export permission granted unconditionally | plugin-security | Get: `canExport` is `true` | `packages/plugins/plugin-security/src/security-plugin.ts#canExport` |
104-
| 6 | Object-level read admission granted unconditionally | plugin-security | Get: `canReadObject` is `true`. This is the OBJECT-level half of a read — "may this caller read this object at all" — which the doors that bypass this middleware ask before they compile a statement of their own; `getReadFilter` is its row-level half, and the two are not interchangeable | `packages/plugins/plugin-security/src/security-plugin.ts#canReadObject` |
105-
| 6b | WRITE admission granted unconditionally | plugin-security | Get: `canWriteObject` is `true`. The WRITE twin of the object-level read admission above, asked by the write PREVIEW (`ObjectQL.validate()`), which runs no middleware for its target object and so has no gate of its own. It exists because a validation rule that reads one hop through a reference field is evaluated against a related row fetched under system authority, and that elevation is bounded to the arms the question RUNS — named, ⛔ never a category of the write decision, and ⛔ not a promise the write would succeed. The arms: the ADR-0103 engine-owned affordance gate and the ADR-0090 D12 delegated-admin gate (both ahead of every resolution), the fail-closed postures (#3545's unresolvable posture, the D10 dangling delegator and, on a walled posture, the D10 delegator who is not a member of the organization the request runs in), the ADR-0066 D3 capability arm, the CRUD grant, the ADR-0090 D10 delegator arm that a bare CRUD-grant check misses, the field-level-security write gate over the caller's payload when one is supplied, and the ADR-0123 D2 no-active-organization wall | `packages/plugins/plugin-security/src/security-plugin.ts#canWriteObject` |
101+
| 3 | Row-level read filter resolves to "no filter" | plugin-security | Get: unscoped reads. Lose: row-level scoping entirely | `packages/plugins/plugin-security/src/security-plugin-read-filter.ts#getReadFilter` |
102+
| 4 | Field-level security returns **all** fields | plugin-security | Get: every column readable (`getReadableFields`) and writable (`getWritableFields`). Lose: field masking | `packages/plugins/plugin-security/src/security-plugin-field-readers.ts#resolveProjectionFieldMask` |
103+
| 5 | Export permission granted unconditionally | plugin-security | Get: `canExport` is `true` | `packages/plugins/plugin-security/src/security-plugin-object-admission.ts#canExport` |
104+
| 6 | Object-level read admission granted unconditionally | plugin-security | Get: `canReadObject` is `true`. This is the OBJECT-level half of a read — "may this caller read this object at all" — which the doors that bypass this middleware ask before they compile a statement of their own; `getReadFilter` is its row-level half, and the two are not interchangeable | `packages/plugins/plugin-security/src/security-plugin-object-admission.ts#canReadObject` |
105+
| 6b | WRITE admission granted unconditionally | plugin-security | Get: `canWriteObject` is `true`. The WRITE twin of the object-level read admission above, asked by the write PREVIEW (`ObjectQL.validate()`), which runs no middleware for its target object and so has no gate of its own. It exists because a validation rule that reads one hop through a reference field is evaluated against a related row fetched under system authority, and that elevation is bounded to the arms the question RUNS — named, ⛔ never a category of the write decision, and ⛔ not a promise the write would succeed. The arms: the ADR-0103 engine-owned affordance gate and the ADR-0090 D12 delegated-admin gate (both ahead of every resolution), the fail-closed postures (#3545's unresolvable posture, the D10 dangling delegator and, on a walled posture, the D10 delegator who is not a member of the organization the request runs in), the ADR-0066 D3 capability arm, the CRUD grant, the ADR-0090 D10 delegator arm that a bare CRUD-grant check misses, the field-level-security write gate over the caller's payload when one is supplied, and the ADR-0123 D2 no-active-organization wall | `packages/plugins/plugin-security/src/security-plugin-object-admission.ts#canWriteObject` |
106106
| 6c | The tenant wall's per-row verdict for the write PREVIEW answers **"not asked"** | plugin-security | Get: `writeWallVerdict` returns `null`, so `ObjectQL.validate()` treats the rows exactly as it does with no verdict registered: no row is refused by the Layer 0 tenant wall, and the elevated preview's `readonly` strips stay skipped as they always were. That is what the elevated write does: the whole security middleware short-circuits before step 3.7, so the wall never judges an elevated row's organization. Lose: nothing the write enforces. The verdict exists so a preview answers a row's organization as its write answers it (#25062), and an elevated write meets no tenant wall to answer for | `packages/plugins/plugin-security/src/security-plugin-write-tenant-wall.ts#writeWallVerdict` |
107107
| 7 | Write bypass = `true`, effective write scope = `org` | plugin-security | Get: widest write scope without holding any capability | `packages/plugins/plugin-security/src/security-plugin.ts#start` |
108108
| 8 | Metadata-plane schema masking exempt (ADR-0106 D4) | metadata-core | Get: unmasked object schema. Note: the exemption is a **caller** property — it short-circuits before the security service is consulted | `packages/metadata-core/src/object-schema-fls.ts#isObjectSchemaMaskExempt` |

‎packages/plugins/plugin-security/src/delegated-organization-membership.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ describe('[#25107] every reader in this package that resolves a delegator asks t
685685
const calls = (text: string, name: string) =>
686686
(text.match(new RegExp(`\\b${name}\\(`, 'g')) ?? []).length - (text.match(new RegExp(`function ${name}\\(`, 'g')) ?? []).length;
687687

688-
it('per source file, the asks equal the resolutions — and the three files that resolve one are all counted', () => {
688+
it('per source file, the asks equal the resolutions — and the six files that resolve one are all counted', () => {
689689
const counted: Record<string, [number, number]> = {};
690690
for (const file of readdirSync(SRC, { recursive: true }) as string[]) {
691691
if (!file.endsWith('.ts') || file.endsWith('.test.ts') || file.includes('.testkit.')) continue;
@@ -694,7 +694,14 @@ describe('[#25107] every reader in this package that resolves a delegator asks t
694694
if (resolutions === 0) continue;
695695
counted[file] = [resolutions, calls(text, 'delegatedOrganizationRefusal')];
696696
}
697-
expect(Object.keys(counted).sort()).toEqual(['explain-engine.ts', 'security-plugin-operation-principals.ts', 'security-plugin.ts']);
697+
expect(Object.keys(counted).sort()).toEqual([
698+
'explain-engine.ts',
699+
'security-plugin-field-readers.ts',
700+
'security-plugin-object-admission.ts',
701+
'security-plugin-operation-principals.ts',
702+
'security-plugin-read-filter.ts',
703+
'security-plugin.ts',
704+
]);
698705
for (const [file, [resolutions, asks]] of Object.entries(counted)) expect(asks, file).toBe(resolutions);
699706
});
700707
});

0 commit comments

Comments
 (0)