Skip to content

Commit c15fd9a

Browse files
refactor(plugin-security): move the security service registration and the seed-ownership claim out of security-plugin.ts (#25223 cut 11) (#25530)
<!-- 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 11 of the `security-plugin.ts` split is the first part of the `start()` row. The claim is 6373426297. Before any claim, the card thread mapped `start()` into four regions and took the plan to the seat, as cut 10's landing note asked. The seat agreed the order: this cut first, through the normal path, then cut 12 (the middleware, Tier H) claimed afresh on the merged `main`. This PR moves two seams into two new modules, unedited. To `packages/plugins/plugin-security/src/security-plugin-security-service.ts`: - `registerSecurityService`, which is ONE STATEMENT of `start()`. It is the `try` that builds the `ISecurityService` literal and registers it as the `security` kernel service. That literal is what every door bypassing the engine middleware asks: the analytics raw-SQL path, the REST export route, the sharing service's management probes, `security/explain` and the metadata plane (ADR-0021 D-C, ADR-0090 D5/D6/D10/D12, ADR-0105 D3/D8, ADR-0106 D7, ADR-0111 D1/D2, ADR-0131 D3). To `packages/plugins/plugin-security/src/security-plugin-seed-claim.ts`: - the body of `claimSeedOwnershipOnSettle`, the seed-ownership claim that `init()` subscribes to `app:seeded` (ADR-0138 D1); - the two readers it asks the published `seed-settlement` contract through, `readSeedSettlementSnapshot` and `readSeededRecordIds`. The change is move-only, and the card stays open for cut 12. The branch is `claude/project-thread-3fzya4`, the harness-pinned branch for this session. It was restarted from `origin/main` at `82f5a111b4`, which contains cut 10's merge. The branch name names no issue, so the claim comment is the identity record. ## How one statement of `start()` moves This is the first cut to move a statement out of a method rather than a whole method body. The seat agreed the plan's form for it before the claim. - The statement is `:2266`-`:2560` at base (295 lines). It closes over three of `start()`'s locals and nothing else: `ctx`, `ql` and `metadata`. It uses no `arguments`, `super`, `new.target` or bare `this`. It has no `return`, `await`, `break` or `continue` at its top level that would act on `start()`. All of that was measured on the AST. - So the function takes the three locals as parameters, typed as the statement saw them there: `PluginContext`, `IObjectQLEngine` and `IMetadataService`. Both handles are already known to be present at that point. Its body is the statement, re-indented one level and otherwise unedited. It returns `void`, as the statement produced no value. - `start()` keeps the statement's position and the comment above it (`:2259`-`:2265`). In its place is one BLOCK: the getter reads, one host literal annotated with the `Pick` of `SecurityServiceHost`, and `registerSecurityService.call(host, ctx, ql, metadata);`. Because it is a block, its `const`s stay inside it and cannot collide with anything later in `start()`. - The statement ran synchronously and still does. The call is in the same place in `start()`'s order: after the invitation placement service is registered and before the middleware is registered. ## What moved The base line numbers are at `82f5a111b4`. The last column lists the members the moved code reads, which is exactly what its host carries. | code | base lines | module | host members | |---|---|---|---| | the `security` service registration (one statement of `start()`) | `:2266`-`:2560` | security-service | methods: `canExport`, `canReadObject`, `checkAuthoredRowWrite`, `checkControlledByParentWrite`, `declareEveryone`, `describePositionGrants`, `explainAccessForCaller`, `getEffectiveObjectPermissions`, `getMetadataReadableFields`, `getObjectSecurityMeta`, `getQueryableFields`, `getReadFilter`, `getReadableFields`, `getWritableFields`, `resolvePermissionSetsForContext`; by value: `acceptedDefaultPermissionSets`, `ownershipFloorAlternates`; getters: `delegatedAdminGate`, `logger`, `permissionEvaluator`, `ql`, `tenancyPosture` | | `claimSeedOwnershipOnSettle` (private, async) | `:1873`-`:1905` | seed-claim | by value: `bootstrapPermissionSets`; getter: `claimTargetAdminUserId` | | `readSeedSettlementSnapshot` (module function, with its docblock) | `:1254`-`:1273` | seed-claim | none | | `readSeededRecordIds` (module function, with its docblock) | `:1275`-`:1293` | seed-claim | none | - `claimSeedOwnershipOnSettle` keeps its name, signature, docblock and position, less `async`. It returns the moved function's promise, as cuts 1 and 3 to 10 did for their async methods. `init()`'s `app:seeded` subscription still calls it on the plugin. - The two readers move with their docblocks, unedited, and gain `export` and nothing else. `security-plugin.ts` imports both back, because R4's promotion-time pass on `kernel:ready` reads both (`:4550`, `:4590`). - Why they move rather than gain `export` where they stood: each signature names a contract type. Cut 6 measured that an export from `security-plugin.ts` whose signature names another module's type reorders the `.d.ts`. So they moved, and the `.d.ts` was measured anyway (below). - Import names that only the moved code reads leave `security-plugin.ts`, 16 in all. The modules import them from the same specifiers. - From the registration: - `resolveDelegatorContext`, `delegatedOrganizationRefusal` and `d10NarrowingStatement` (`./explain-engine.js`); - `listAudienceBindingSuggestions`, `confirmAudienceBindingSuggestion`, `dismissAudienceBindingSuggestion` and the types `SuggestionDeps` and `SuggestionListFilter` (`./suggested-audience-bindings.js`); - the type `OwnershipFloorAlternate` (`./platform-ownership-policies.js`); - the type `ObjectAccessScope` (`@objectstack/spec/security`); - the types `ISecurityService` and `DelegationNarrowing` (`@objectstack/spec/contracts`). - From the seed claim: - `SEED_SETTLEMENT_SERVICE` and the type `ISeedSettlementService` (`@objectstack/spec/contracts`); - `findExistingPlatformAdmin` (`./bootstrap-platform-admin.js`); - the whole `./claim-seed-ownership.js` import (`claimSeedOwnership`). - Disclosed: one staying docblock, `computeDelegatedReadFilter`'s, names `resolveDelegatorContext` in a `{@link}`. The name is no longer imported, so that link is now plain text, like the bare-member `{@link computeRlsFilter}` beside it. No docblock is edited. Every other staying mention of a dropped name is in backticks or prose. - Each value import is read only inside a function body, at call time, never while a module evaluates. `index.ts` exports only what it did, so the published surface does not move (measured below). ## The shape (#23050 shape B, as cuts 1 to 10 applied it) - `SecurityServiceHost` and `SeedClaimHost` 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 code 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. Every member of the registered service that binds a plugin method was already an arrow calling `this.m(…)`. `this.m` is now the host's closure, which calls the plugin's method on the plugin. So every call reaches the same method on the same receiver, and a spy or test double set on the plugin is still the one the service reaches. - Fields follow the rule cut 1 set, judged on each field's own declaration: - `acceptedDefaultPermissionSets`, `ownershipFloorAlternates` and `bootstrapPermissionSets` are declared `readonly`, so they go by value: the same object references. - `delegatedAdminGate`, `logger`, `permissionEvaluator`, `ql`, `tenancyPosture` and `claimTargetAdminUserId` are not. So they go as getters, and each read happens when the code makes it. For example, the posture the suggestion deps hand out is still read per call, and the claim still reads the admin R4 names when it runs. - None of them is function-valued: each is an object whose own methods the code calls, or a value it reads. No receiver changes. - The hosts declare every field `readonly`, so moved code cannot assign a member through them. The typecheck passes, so none does. Neither moved region writes a member. - Nothing re-exports either module, so the package entry does not move. ## Three readers read the moved lines by path, and all three are re-pointed in this PR These are the three the seat named. Each was run with its edit reverted, and each fails without it. - `content/docs/permissions/system-context.mdx`, row 7. The registration holds 3 of `start()`'s 4 `isSystem` reads: `hasWriteBypass`'s, `resolveWriteScope`'s and `describeDelegationNarrowing`'s system short-circuits. `check-system-context-census` keys each read by file and enclosing symbol, so row 7's anchor changes from `security-plugin.ts#start` to `security-plugin-security-service.ts#registerSecurityService`. - The symbol changes because the reads now sit in the function. The row's text and every other anchor stay. Rows 1, 2 and 30 keep `#start`, which still holds the middleware's read. - With the page untouched, the census reports 1 problem: `[file-without-a-row]` for the new module. With the re-point it exits 0. - `packages/plugins/plugin-security/src/delegated-organization-membership.test.ts`, the #25107 enumeration pin. The registration holds the only delegator resolution in `security-plugin.ts`, and the organization ask beside it. So in the expected list, `security-plugin-security-service.ts` replaces `security-plugin.ts`, and the count stays at seven files. - The rule it pins is unchanged: asks equal resolutions in every file. It holds: the new module has 1 and 1, and `security-plugin.ts` has 0 and 0. - Unedited, it fails on the list. - `packages/core/src/security/row-serving-door-exposure.pin.test.ts`. Every file that calls `canReadObject` must be classified, and the service literal's binding `this.canReadObject(…)` now sits in the new module. - The new module gains a `not-a-door` entry: "the provider's service registration: it binds the admission into the `security` service the plugin registers; it serves no rows". - `security-plugin.ts` stays a caller, through the stub's closure and `describePositionGrants`' own ask. Its reason no longer says it binds the admission into the service. It now says "the provider: it implements the admission, hands it to its service registration, and asks it itself only to decide whether `describePositionGrants` may count a position's holders; it serves no rows". - Unedited, it fails with the new module unclassified. ## One new anchor entry `scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-seed-claim.ts.json` (`adrs: ["ADR-0138"]`). - Its invariant is the ADR-0138 D1 paragraph of the `security-plugin.ts` entry, verbatim. That paragraph names `claimSeedOwnershipOnSettle` and `readSeededRecordIds`, whose bodies now sit in the module. Cut 9 carried ADR-0106 D7 into `security-plugin-field-readers.ts` the same way. - The `security-plugin.ts` entry is unchanged and stays true: the method, the promotion-time pass and the import of `readSeededRecordIds` stay in that file. - The security-service module gets no entry, because no paragraph describes the registration: - ADR-0055 §1 is the relation reader; - ADR-0106 D7 is the readable-field asymmetry, in the field-readers module since cut 9; - ADR-0123 D2 is step 3.7's refusal, in the middleware. `docs/adr/**` is not touched, and no other governed path is, so this is a normal landing. ## The published `.d.ts` does not change Measured under the series bar (ruling 6348891160 item 1), on base `82f5a111b4` and on head `5a4747b9c7`. Each was a full `turbo run build` in its own worktree (both exit 0). - Raw sha256 of `dist/index.d.ts` and `dist/index.d.mts` is the same for all four files, base and head: `5e7e8df69375e6606767eee938111aaaf0b9bac4dd9c728689c343be9e5f7702` (14,696 lines). The raw diff is 0 lines, so no union order moved either. - Normalised sha256 is the same for all four: `1e2e8299c2b3aa8502ae3684a5a8a4f7148b2211876a0a581f20d27677531ed9`. - `security-plugin.ts` gains no export. The two readers gained `export` in the new module, which nothing re-exports, so the cut 6 trap does not arise. - The normaliser, run from `packages/plugins/plugin-security`, 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 control, a real package build rather than an edit of the emitted file. With `private` removed from `claimSeedOwnershipOnSettle` in the head source, `pnpm --filter @objectstack/plugin-security build` emits line 431 as the full public signature instead of `private claimSeedOwnershipOnSettle;`. That gives raw `12beec21603e70025c7a1b70d2c49fd0dd110d66a61b57e82098a6eb903fe08c` and normalised `0509461ad52776d160e6875c3a99cee28dda954089413b05db158aabda6d52ff`, both different from the real hashes. So the comparison does see a visibility change. With the source restored, a rebuild gives `5e7e8df6…` again for both files. - `claimSeedOwnershipOnSettle` is private, so the `.d.ts` carries only its name and docblock (line 431). `registerSecurityService` and the two readers are in no published file. - Export tables: the ESM and CJS bundles each export the same 66 names at base and at head. The lists were loaded at runtime, with `require` and `import()` of the built bundles, not parsed. The sorted name list hashes to `83576430f83cf60cecbcc5c0e80214f16c626a8e66f700294fc6a44361423116` for all four, as at cuts 6 to 10. - The bundle JS bytes differ, because code moved within them: `index.js` goes from `a0d05e78` to `b734b9f0`, and `index.mjs` from `1080b070` to `51498bc7`. Every value the new modules import comes from a file inside this package or from a specifier `security-plugin.ts` already imports. So the set of external specifiers each bundle loads is the same at base and head: 17 distinct `require(…)` specifiers in `index.js` and 24 distinct `from` specifiers in `index.mjs`, counted the same way at both ends. - Bundle order, disclosed. The seed-claim module imports `bootstrap-platform-admin.ts` and `claim-seed-ownership.ts`. So those two, and `platform-admin-standing-audit.ts`, are now emitted after the two new modules and ahead of `ownership-floor-alternates.ts` and `security-plugin.ts`'s first chunk, instead of after `predicate-guard.ts`. None of the three imports `security-plugin.ts`. Their top level only declares (one constant is computed from an imported constant, `CLAIM_PAGE_ROWS`). No changeset: `skip-changeset`, per ruling 6348891160 item 4. ## Proof of move-only - **The statement, by AST token.** `registerSecurityService`'s body is one statement, with the same TypeScript token sequence as the base `try` statement: 1,296 tokens. Each of its 295 lines is the base line with two leading spaces removed. Multi-line JSDoc text tokens inside it are re-indented one level, like every other line, so the comparison dedents those tokens one level, and only those. - **`start()` outside the statement.** The head text of `start()` equals the base text exactly, outside the one statement, which is now a 33-line stub block ending in the `.call`. Its signature, docblock and every other statement are byte-identical. That covers R1, the comment above the statement, the middleware and R4. - **The method, by AST token.** `claimSeedOwnershipOnSettle`'s function body has the same token sequence as the base method body (136 tokens), and each of its 28 lines is the base line less two spaces. The parameters, the return type and `async` match. The class keeps the method's docblock and signature, less `async`. - **The two readers.** Each one's text in the module, docblock included, is the base text with `export` added, and nothing else. Neither is left in `security-plugin.ts`. - **Class side.** Every other class member is byte-identical to base: 124 named members plus the constructor. The class has 127 members at base and at head. - **Top level of `security-plugin.ts`.** Of the 110 top-level statements at base other than the class, 101 are byte-identical at head. The other nine are: - six imports that each lose the names listed above; - the `./claim-seed-ownership.js` import, which is gone; - the two readers, which are gone from the file. The two new statements are the imports of the new modules, with their comments. The check asserts the exact list of 16 dropped names and 6 added names, and that no dropped name is still referenced in the file. - **Negative controls.** Seven fail the check: - in the registration, `ctx.logger.info(` changed to `ctx.logger.debug(`; - in the registration, one line indented by one more space, with the tokens unchanged; - in `start()`, one edit in R1 (`:1914`), ahead of the stub; - in `start()`, one edit in the middleware, after the stub; - in `readSeedSettlementSnapshot`, `return undefined` changed to `return null`; - in `claimSeedOwnershipOnSettle`, `ctx.logger.warn(` changed to `ctx.logger.error(`; - in a top-level docblock of `security-plugin.ts`, one trailing space added. - **git's own view.** This is `git diff --color-moved=plain --color-moved-ws=allow-indentation-change 82f5a11 HEAD` over the three source files. - Of the 378 deleted lines, git sees 370 as moved. The other 8 are four type-import lines and the `./bootstrap-platform-admin.js` import line, which the modules group differently; the two reader signatures that gain `export`; and the method signature that loses `async`. - Of the 613 added lines, 391 show as moved: 311 in the security-service module, 79 in the seed-claim module, and 1 in `security-plugin.ts` (a stub's `};`). The rest are the modules' headers, imports, host interfaces and function signatures, the two stubs, and the two new imports with their comments. - **Size.** 1,010 changed lines (+629 / -381) over 7 files, under the 3,000-line manual-merge line. - `security-plugin.ts` goes from 8,442 to 8,120 lines. - `security-plugin-security-service.ts` has 418 lines, and `security-plugin-seed-claim.ts` 139. - The other four files are the page (+1 / -1), the two pins (+1 / -1 and +7 / -1) and the anchor entry (+7). ## ADR anchors (ruling 6353073966 item 2) - `node scripts/check-adr-symbol-anchors.mjs` exits 0 and gives the same output at base `82f5a111b4` and at head. - `pnpm check:adr-anchors` exits 0 at base and at head. Anchored files go from 82 to 83, the new entry. - **The decision-letter count on the anchored surface goes from 1970 to 1958.** That gate counts `ADR-NNNN Dk` citations in each anchored file and in each entry's own invariant text. The −12 is: - −13: the registration's letters leave the anchored surface with it (ADR-0090 D5 three times, D6, D10 and D12; ADR-0105 D3 and D8; ADR-0106 D7 twice; ADR-0111 D1 and D2; ADR-0131 D3); - 0: `readSeededRecordIds`' docblock carries ADR-0138 D1, and it stays on the anchored surface, now in an anchored module; - +1: the new entry's invariant text. The method body, the module headers, the stubs and the new import comments cite none. Measured per file: `security-plugin.ts` holds 250 such citations at base and 236 at head. The security-service module holds the 13, and the seed-claim module the 1. - Every moved citation is still in the tree, in the module its code moved to. Citations that resolve tree-wide go from 52262 across 6671 files at base to 52264 across 6674 at head. The two added citations are the new entry's `adrs` list and its invariant. The three added files are the two modules and the entry. - `docs/adr/**` is not touched, and no anchor token is planted. - The anchor map covers every ADR symbol anchor that names `security-plugin.ts`: 18 symbols. - It was measured with `extractAnchors` and `symbolSegmentResolution` from `scripts/symbol-anchors.mjs`. That is measurement, not a new gate. - It was read on `82f5a111b4` BEFORE the claim, with the whole cut applied as a local trial, and no anchor changed class. - No ADR anchors the moved method, the two readers or the new function. Two anchored members are named inside the service literal: `#getReadFilter` and `#SecurityPlugin.getMetadataReadableFields`. Their names now also appear in the stub's host literal and its `Pick`. Both still resolve as `declaration`, on the methods. The table gives each anchor's class at base and at 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 · `:746` (top-level) | declaration · `:748` (top-level) | | `#SecurityPlugin` | ADR-0049:49, ADR-0138:248, ADR-0138:265 | declaration · `:1348` (class) | declaration · `:1309` (class) | | `#SecurityPlugin.getMetadataReadableFields` | ADR-0138:153 | declaration · `:5620` (method) | declaration · `:5298` (method) | | `#SecurityPlugin.resolvePermissionSetsForContextUnmemoized` | ADR-0138:427 | declaration · `:6214` (method) | declaration · `:5892` (method) | | `#assertControlledByParentWrite` | ADR-0055:58, ADR-0055:125 | declaration · `:7699` (method) | declaration · `:7377` (method) | | `#assertMasterRowEditable` | ADR-0055:125 | declaration · `:7825` (method) | declaration · `:7503` (method) | | `#collectRLSPolicies` | ADR-0105:93 | declaration · `:7879` (method) | declaration · `:7557` (method) | | `#composeWriteTenantWall` | ADR-0123:4 | declaration · `:7037` (method) | declaration · `:6715` (method) | | `#computeControlledByParentFilter` | ADR-0055:5, ADR-0055:123, ADR-0139:246 | declaration · `:7345` (method) | declaration · `:7023` (method) | | `#computeDelegatedReadFilter` | ADR-0139:223 | declaration · `:5539` (method) | declaration · `:5217` (method) | | `#computeRlsFilter` | ADR-0055:24 | declaration · `:6745` (method) | declaration · `:6423` (method) | | `#getReadFilter` | ADR-0096:181, ADR-0096:182, ADR-0096:183, ADR-0096:199, ADR-0139:222 | declaration · `:5485` (method) | declaration · `:5163` (method) | | `#hasPlatformAdminPosture` | ADR-0099:317 | declaration · `:6772` (method) | declaration · `:6450` (method) | | `#isPrincipalLessContext` | ADR-0138:152 | declaration · `:619` (top-level) | declaration · `:621` (top-level) | | `#manage_metadata` | ADR-0099:68 | literal · `:372` (top-level) | literal · `:374` (top-level) | | `#nav_permission_sets` | ADR-0126:406 | literal · `:1776` (`init`) | literal · `:1737` (`init`) | | `#rlsFilter` | ADR-0055:24 | declaration · `:4360` (`start`) | declaration · `:4038` (`start`) | | `#writeCheckPolicies` | ADR-0066:112 | declaration · `:1243` (top-level) | declaration · `:1245` (top-level) | ## Readers The three readers above are the only ones that read the moved lines by path. No other gate, baseline, docs anchor or test does. Each kind was read: - **The system-context census.** - `node scripts/isystem-census.mjs --json` gives the same 149 sites at base and at head, with 0 differences. The comparison ignores line numbers, maps the two modules back to `security-plugin.ts`, and maps the symbol `registerSecurityService` back to `start`. - Three head sites are in the security-service module: the three short-circuits, which the re-pointed row 7 names. - The role counts are the same (`read` 154, `declaration` 39, `key` 440, `other` 9), and so are the 5 non-elevation reads. Files holding a site go from 76 to 77, and scanned files from 402 to 403. - `check-system-context-census` and its `--self-test` exit 0 at base and at head. The totals: 149 sites at both; symbols 128 to 129; cited 141 to 142 against as many required; files 76 to 77. With the page untouched, it reports 1 problem (above). - **The tenant-audit census.** No moved code holds an engine write. The seed claim writes through `claimSeedOwnership`, which stays in its own file, and the registration only reads. - `node scripts/tenant-audit-census.mjs --json` gives the same 238 write call sites at base and at head, compared the same way: 0 differences, none in the modules, and the same 176 non-engine calls. Only `scannedSources` moves, from 739 to 741: the two modules. - `check-tenant-audit-census` exits 0 at base and at head, and `tenant-audit-census.mdx` is not touched. - **The two pins** are described above. `operation-private-keys.pin.test.ts` (`@objectstack/core`) names no moved code, and it passes at head. - **Symbol anchors outside the ADRs.** Across the ADR, spec liveness, platform checklist, system-context page and spec `conversions/registry.ts` corpora, `security-plugin.ts#X` is cited 48 times over 26 symbols at base and 47 at head. With `symbolSegmentResolution`, each resolves at head with its base class: 0 changed. The one ref that leaves is row 7's. - `check-platform-checklist` (the script and `pnpm check:platform-checklist`), `check-scripts-symbol-anchors` and its `--self-test`, and `pnpm --filter @objectstack/spec check:liveness` were run at base and at head (Gates). - **Readers by name, not by path.** These were read, and none needs a change: - the behaviour tests of the `security` service and the seed claim in this package. Each drives the plugin or the registered service, and none calls the moved code directly. No test replaces `claimSeedOwnershipOnSettle` on the plugin instance; - the page's row text and the docblocks that stay in the class, which name these members and stay true by name. - **Line citations.** No `security-plugin.ts:N` citation in the repo points into the moved ranges (base `:1254` to `:1293`, `:1873` to `:1905` and `:2266` to `:2560`). ## Gates All gates ran against base `82f5a111b4` and on head `5a4747b9c7`, each in its own worktree after a full `turbo run build` there (both exit 0). The CI run on this head is green: 36 checks passed and 11 were skipped by their path filters. - `pnpm --filter @objectstack/plugin-security test` (`vitest run`): Test Files 218 passed (218); Tests 4573 passed, 45 skipped (4618). Base `82f5a111b4` gives the same counts. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0 at head and at base. It runs `tsc --noEmit`, the scripts tsconfig, and `check:test-typecheck`. - eslint over the five touched source and test files: exit 0. - `pnpm --filter @objectstack/plugin-security build`: exit 0. It produced the `.d.ts` and the bundles measured above. - `row-serving-door-exposure.pin.test.ts` and `operation-private-keys.pin.test.ts` (`@objectstack/core`): 8 of 8 pass at head and at base. - `dispatch-gates --commands`: 101 commands. The `--ran` reconciliation shows 101 derived, 101 run, 0 NOT-MEASURED and 0 UNRUN. The zero is derived: all 101 recorded an exit code, and none is 3. - Six commands are not derived for this change set. Each was run by hand: - `node scripts/check-adr-symbol-anchors.mjs`; - `check-platform-checklist`, both the script and `pnpm check:platform-checklist`; - the two census `--json` reads; - `pnpm --filter @objectstack/spec check:liveness`. `pnpm check:adr-anchors`, `check-scripts-symbol-anchors` and its `--self-test`, and `pnpm check:query-options-erasure` are derived for this change set this time. - Nine more were run at base and at head: - Ten artifact-roster families keep their roster in a directory this change touches, so `dispatch-gates` marks their silence as no evidence. Two of them, `check:engine-double-contract` and `check:i18n-stale-fill`, are derived anyway. The other eight were run: `check-published-list-mirrors` and its `--self-test`, `check:authz-resolver`, `check:console-injection`, `check:error-code-casing`, `check:filter-alias-parity`, `check:published-readme-exports` and `check-dts-references --self-test`. - The ninth is the package typecheck. All of them exit 0 at both ends. - **Base against head, all 118 commands.** All 118 exit 0 at head and at base. 88 give identical output at both, once the worktree path and timings are masked. The other 30, each read line by line: - These move as described above: `check:adr-anchors`, `check-system-context-census` and the two census `--json` reads. The tenant read moves only by `scannedSources` and its own ref. - The plugin-security suite's console output is not stable between runs: log lines from parallel workers, generated ids and timestamps. Its stack frames into `security-plugin.ts` carry the file's new line numbers. Its counts are the same at both ends (above). - `check-issue-citations` judges a change set, so it has nothing to read at base. At head it judged 22 citations across 3 files, and all resolve. - These counts move by the two modules (and the anchor entry, for the whole-index readers): - `check-closing-keyword-parity` 12065 to 12068 tracked files; - `check:gitlink-declared` 12065 to 12068 index entries; - `check:nul-bytes` 12056 to 12059; - `check-comment-mask-corpus` 10030 to 10032; - `check-plugin-teardown-shape` 9421 to 9423 sources; - `check-position-name-fold-loaders` 12399 to 12402 files, with the same 8 references; - `check-undeclared-dep-imports` 3326 to 3328 files; - the spec package's `check:duration-unit-keys` 3331 to 3333; - `check:closed-ceremony-text` 4594 to 4596; - `check:doc-authoring` 1445 to 1447 parsed sources; - `check:engine-double-contract` 3636 to 3638 production files; - `check:error-status-conformance` 3424 to 3426, with the same 510 producer sites; - `check:error-code-casing` 9404 to 9406; - `check:logger-receiver-detach` 3644 to 3646; - `check:org-identifier` 3669 to 3671; - `check:page-declaration-shape` 3677 to 3679; - `check:refd-timer-probe` 10023 to 10025; - `check:type-check-coverage` and `check:type-check-debt` 3553 to 3555 walked sources; - `check:vendor-version-stamps` 10422 to 10424; - `check:agent-test-spelling` 613 to 614 files, with the same tokens and runs; - `check:pnpm-flag-spellings` 454 to 455 files under `scripts/` (the anchor entry), with the same scripts read. - `check-undeclared-dep-imports` also reads 2885 to 2890 `@objectstack/*` specifiers. Those are the ones the two modules import, all declared dependencies. `security-plugin.ts` keeps every specifier it still uses. - `check:doc-authoring` also reads 105072 to 105157 strings, the strings of the new headers and stubs. - `check:skill-identifier-liveness` reads 140073 to 140077 implementation word tokens. Those are the four words new to the tree: `SecurityServiceHost`, `SeedClaimHost`, `registerSecurityService` and the stub's getter local `readClaimTargetAdminUserId`. No citation changes. - `check:merge-driver` differs only in its temporary directory names. - The Docs Drift Check comment lists pages that name identifiers in the moved text. The change is move-only, so none of them describes behaviour that changed, and no page is edited. Per command (exit code at head, then at base), the derived first, then the hand-run, the roster and the two suites: - `node scripts/check-ci-filter-parity.mjs` exit 0 (base 0) - `node scripts/check-closing-keyword-parity.mjs` exit 0 (base 0) - `node scripts/check-closing-keyword-parity.mjs --self-test` exit 0 (base 0) - `node scripts/check-comment-mask-adoption.mjs` exit 0 (base 0) - `node scripts/check-comment-mask-adoption.mjs --self-test` exit 0 (base 0) - `node scripts/check-comment-mask-corpus.mjs` exit 0 (base 0) - `node scripts/check-dev-prereqs.mjs --self-test` exit 0 (base 0) - `node scripts/check-doc-frontmatter.mjs` exit 0 (base 0) - `node scripts/check-doc-frontmatter.mjs --self-test` exit 0 (base 0) - `node scripts/check-doc-route-spelling.mjs --advisory` exit 0 (base 0) - `node scripts/check-doc-route-spelling.mjs --self-test` exit 0 (base 0) - `node scripts/check-docs-section-name.mjs` exit 0 (base 0) - `node scripts/check-docs-section-name.mjs --self-test` exit 0 (base 0) - `node scripts/check-dts-emitted.mjs --self-test` exit 0 (base 0) - `node scripts/check-issue-citations.mjs` exit 0 (base 0) - `node scripts/check-keyed-text-bounds.mjs` exit 0 (base 0) - `node scripts/check-keyed-text-bounds.mjs --self-test` exit 0 (base 0) - `node scripts/check-platform-object-tenancy-census.mjs` exit 0 (base 0) - `node scripts/check-platform-object-tenancy-census.mjs --self-test` exit 0 (base 0) - `node scripts/check-plugin-teardown-shape.mjs` exit 0 (base 0) - `node scripts/check-plugin-teardown-shape.mjs --self-test` exit 0 (base 0) - `node scripts/check-position-name-fold-loaders.mjs` exit 0 (base 0) - `node scripts/check-position-name-fold-loaders.mjs --self-test` exit 0 (base 0) - `node scripts/check-registry-log-declared.mjs` exit 0 (base 0) - `node scripts/check-registry-log-declared.mjs --self-test` exit 0 (base 0) - `node scripts/check-rest-log-spy-declared.mjs` exit 0 (base 0) - `node scripts/check-rest-log-spy-declared.mjs --self-test` exit 0 (base 0) - `node scripts/check-scripts-symbol-anchors.mjs` exit 0 (base 0) - `node scripts/check-scripts-symbol-anchors.mjs --self-test` exit 0 (base 0) - `node scripts/check-section-landing-index.mjs` exit 0 (base 0) - `node scripts/check-section-landing-index.mjs --self-test` exit 0 (base 0) - `node scripts/check-system-context-census.mjs` exit 0 (base 0) - `node scripts/check-system-context-census.mjs --self-test` exit 0 (base 0) - `node scripts/check-tenant-audit-census.mjs` exit 0 (base 0) - `node scripts/check-tenant-audit-census.mjs --self-test` exit 0 (base 0) - `node scripts/check-undeclared-dep-imports.mjs` exit 0 (base 0) - `node scripts/check-undeclared-dep-imports.mjs --self-test` exit 0 (base 0) - `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` exit 0 (base 0) - `node scripts/docs-audit/check-affected-docs.mjs` exit 0 (base 0) - `node scripts/docs-audit/check-drift-comment.mjs` exit 0 (base 0) - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` exit 0 (base 0) - `pnpm --filter @objectstack/lint run check:doc-security-posture` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:docs` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:duration-unit-keys` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:empty-state` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:liveness` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:skill-examples` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:strictness-ledger` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:variant-docs` exit 0 (base 0) - `pnpm --filter @objectstack/spec run check:yaml-examples` exit 0 (base 0) - `pnpm check:adr-anchors` exit 0 (base 0) - `pnpm check:agent-test-spelling` exit 0 (base 0) - `pnpm check:bash32-floor` exit 0 (base 0) - `pnpm check:cli-command-ids` exit 0 (base 0) - `pnpm check:closed-ceremony-text` exit 0 (base 0) - `pnpm check:corpus-claim-drift` exit 0 (base 0) - `pnpm check:cross-package-test-inputs` exit 0 (base 0) - `pnpm check:doc-anchors` exit 0 (base 0) - `pnpm check:doc-authoring` exit 0 (base 0) - `pnpm check:docs-audit-scope` exit 0 (base 0) - `pnpm check:docs-redirects` exit 0 (base 0) - `pnpm check:docs-single-h1` exit 0 (base 0) - `pnpm check:docs-spec-enumerations` exit 0 (base 0) - `pnpm check:docs-transcript-drift` exit 0 (base 0) - `pnpm check:driver-memory-census` exit 0 (base 0) - `pnpm check:dts-closure` exit 0 (base 0) - `pnpm check:dual-build-cjs-loads` exit 0 (base 0) - `pnpm check:engine-double-contract` exit 0 (base 0) - `pnpm check:entry-guard` exit 0 (base 0) - `pnpm check:error-status-conformance` exit 0 (base 0) - `pnpm check:gitlink-declared` exit 0 (base 0) - `pnpm check:i18n` exit 0 (base 0) - `pnpm check:i18n-stale-fill` exit 0 (base 0) - `pnpm check:issue-citations` exit 0 (base 0) - `pnpm check:kernel-hook-pairs` exit 0 (base 0) - `pnpm check:lean-entry-closure` exit 0 (base 0) - `pnpm check:logger-receiver-detach` exit 0 (base 0) - `pnpm check:merge-driver` exit 0 (base 0) - `pnpm check:nul-bytes` exit 0 (base 0) - `pnpm check:objectql-double-limit` exit 0 (base 0) - `pnpm check:org-identifier` exit 0 (base 0) - `pnpm check:page-declaration-shape` exit 0 (base 0) - `pnpm check:parse-guard` exit 0 (base 0) - `pnpm check:pnpm-filter-targets` exit 0 (base 0) - `pnpm check:pnpm-flag-spellings` exit 0 (base 0) - `pnpm check:published-files` exit 0 (base 0) - `pnpm check:published-readme-links` exit 0 (base 0) - `pnpm check:query-options-erasure` exit 0 (base 0) - `pnpm check:react-page-adapter-contract` exit 0 (base 0) - `pnpm check:refd-timer-probe` exit 0 (base 0) - `pnpm check:role-word` exit 0 (base 0) - `pnpm check:skill-identifier-liveness` exit 0 (base 0) - `pnpm check:slot-lookup` exit 0 (base 0) - `pnpm check:sourcemap-no-sources-content` exit 0 (base 0) - `pnpm check:test-source-alias` exit 0 (base 0) - `pnpm check:tier-file-adoption` exit 0 (base 0) - `pnpm check:type-check-coverage` exit 0 (base 0) - `pnpm check:type-check-debt` exit 0 (base 0) - `pnpm check:vendor-version-stamps` exit 0 (base 0) - `pnpm check:watch-hint-literal` exit 0 (base 0) - `pnpm check:where-matcher` exit 0 (base 0) - `node scripts/check-adr-symbol-anchors.mjs` exit 0 (base 0) - `node scripts/check-platform-checklist.mjs` exit 0 (base 0) - `pnpm check:platform-checklist` exit 0 (base 0) - `node scripts/isystem-census.mjs --json` exit 0 (base 0) - `node scripts/tenant-audit-census.mjs --json` exit 0 (base 0) - `pnpm --filter @objectstack/spec check:liveness` exit 0 (base 0) - `node scripts/check-published-list-mirrors.mjs` exit 0 (base 0) - `pnpm check:authz-resolver` exit 0 (base 0) - `pnpm check:console-injection` exit 0 (base 0) - `pnpm check:error-code-casing` exit 0 (base 0) - `pnpm check:filter-alias-parity` exit 0 (base 0) - `pnpm check:published-readme-exports` exit 0 (base 0) - `pnpm --filter @objectstack/plugin-security typecheck` exit 0 (base 0) - `pnpm --filter @objectstack/plugin-security test` exit 0 (base 0) - `pnpm --filter @objectstack/core exec vitest run src/security/row-serving-door-exposure.pin.test.ts src/security/operation-private-keys.pin.test.ts` exit 0 (base 0) - `node scripts/check-dts-references.mjs --self-test` exit 0 (base 0) - `node scripts/check-published-list-mirrors.mjs --self-test` exit 0 (base 0) ## Acceptance notes - Nothing outside this cut is touched: no other section of the class or of `start()`, no behaviour, no `docs/adr/**`, no census ledger and no `packages/spec/**`. Outside the package, two files change: the one anchor path on the system-context page, and the core pin's classification. - Serial check, also in the claim: of the 9 open PRs, one touches `security-plugin.ts`: #25528 (#25234's second PR, another project's). It adds one import after the rls-filter import (`:173` here), a loop in R1 after the resolver's registration log (`:2167` here), and edits `stageRlsMembership`. None of those lines is in the registration, the seed claim, the readers or an import line this cut edits. A scratch merge of #25528's head with this cut is clean (`git merge-tree`, exit 0). No open PR touches the two module names, `system-context.mdx`, the two pins, the census scripts or the plugin-security anchor entries. - After this cut, `start()` holds R1, the registration's stub block, the middleware (R3) and R4. Next is cut 12: R3 moves out on a fresh claim on the merged `main`. It is Tier H on two counts, the ADR-0055:24 `#rlsFilter` re-anchor (ruling 6353073966 item 5) and its size, and its claim says so up front. --- _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 5ea211c commit c15fd9a

7 files changed

Lines changed: 629 additions & 381 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ that silently does not happen.
105105
| 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` |
106106
| 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` |
107107
| 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` |
108-
| 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` |
108+
| 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-security-service.ts#registerSecurityService` |
109109
| 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` |
110110
| 8b | Authoring-trail reads admitted | rest, runtime | Get: a metadata item's version history, audit rows and stored-version diff (`GET /meta/:type/:name/history`, `/audit`, `/diff`) without `studio.access` or `manage_metadata`, and a package's commit timeline (`GET /packages/:id/commits`) without `studio.access`. Note: narrower than the schema-mask exemption the draft switches ask (`isObjectSchemaMaskExempt`): neither door admits `setup.access`, which organization admins hold | `packages/rest/src/rest-server.ts#mayReadAuthoringTrail`, `packages/runtime/src/domains/packages.ts#requireCommitTrailReadCapability` |
111111
| 9 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `packages/plugins/plugin-security/src/security-plugin-explain-access.ts#explainAccessForCaller` |

‎packages/core/src/security/row-serving-door-exposure.pin.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,13 @@ const CALLERS: Record<string, Classification> = {
9393
},
9494
'packages/plugins/plugin-security/src/security-plugin.ts': {
9595
kind: 'not-a-door',
96-
reason: 'the provider: it implements the admission and binds it into the security service it registers; it serves no rows',
96+
reason:
97+
'the provider: it implements the admission, hands it to its service registration, and asks it itself only to '
98+
+ 'decide whether `describePositionGrants` may count a position\'s holders; it serves no rows',
99+
},
100+
'packages/plugins/plugin-security/src/security-plugin-security-service.ts': {
101+
kind: 'not-a-door',
102+
reason: 'the provider\'s service registration: it binds the admission into the `security` service the plugin registers; it serves no rows',
97103
},
98104
'packages/objectql/src/engine.ts': {
99105
kind: 'not-a-door',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ describe('[#25107] every reader in this package that resolves a delegator asks t
701701
'security-plugin-object-admission.ts',
702702
'security-plugin-operation-principals.ts',
703703
'security-plugin-read-filter.ts',
704-
'security-plugin.ts',
704+
'security-plugin-security-service.ts',
705705
]);
706706
for (const [file, [resolutions, asks]] of Object.entries(counted)) expect(asks, file).toBe(resolutions);
707707
});

0 commit comments

Comments
 (0)